diff --git a/App.axaml b/App.axaml
new file mode 100644
index 0000000..c6ae445
--- /dev/null
+++ b/App.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/App.axaml.cs b/App.axaml.cs
new file mode 100644
index 0000000..c6f60f6
--- /dev/null
+++ b/App.axaml.cs
@@ -0,0 +1,23 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using CRD.ViewModels;
+using MainWindow = CRD.Views.MainWindow;
+
+namespace CRD;
+
+public partial class App : Application{
+ public override void Initialize(){
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted(){
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop){
+ desktop.MainWindow = new MainWindow{
+ DataContext = new MainWindowViewModel(),
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/Assets/Icons.axaml b/Assets/Icons.axaml
new file mode 100644
index 0000000..4ff874f
--- /dev/null
+++ b/Assets/Icons.axaml
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/Assets/app_icon.ico b/Assets/app_icon.ico
new file mode 100644
index 0000000..66ef3af
Binary files /dev/null and b/Assets/app_icon.ico differ
diff --git a/Downloader/CRAuth.cs b/Downloader/CRAuth.cs
new file mode 100644
index 0000000..244551b
--- /dev/null
+++ b/Downloader/CRAuth.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using System.Web;
+using CRD.Utils;
+using CRD.Utils.Structs;
+using Newtonsoft.Json;
+using YamlDotNet.Core.Tokens;
+
+namespace CRD.Downloader;
+
+public class CrAuth(Crunchyroll crunInstance){
+ public async Task AuthAnonymous(){
+ var formData = new Dictionary{
+ { "grant_type", "client_id" },
+ { "scope", "offline_access" }
+ };
+ var requestContent = new FormUrlEncodedContent(formData);
+ requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+
+ var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){
+ Content = requestContent
+ };
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ JsonTokenToFileAndVariable(response.ResponseContent);
+ } else{
+ Console.WriteLine("Anonymous login failed");
+ }
+
+ crunInstance.Profile = new CrProfile{
+ Username = "???",
+ Avatar = "003-cr-hime-excited.png",
+ PreferredContentAudioLanguage = "ja-JP",
+ PreferredContentSubtitleLanguage = "de-DE"
+ };
+
+ Crunchyroll.Instance.CmsToken = new CrCmsToken();
+
+ }
+
+ private void JsonTokenToFileAndVariable(string content){
+ crunInstance.Token = JsonConvert.DeserializeObject(content, crunInstance.SettingsJsonSerializerSettings);
+
+
+ if (crunInstance.Token != null && crunInstance.Token.expires_in != null){
+ crunInstance.Token.expires = DateTime.Now.AddMilliseconds((double)crunInstance.Token.expires_in);
+
+ CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken);
+ }
+ }
+
+ public async Task Auth(AuthData data){
+ var formData = new Dictionary{
+ { "username", data.Username },
+ { "password", data.Password },
+ { "grant_type", "password" },
+ { "scope", "offline_access" }
+ };
+ var requestContent = new FormUrlEncodedContent(formData);
+ requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+
+ var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){
+ Content = requestContent
+ };
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ JsonTokenToFileAndVariable(response.ResponseContent);
+ }
+
+ if (crunInstance.Token?.refresh_token != null){
+ HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
+
+ await GetProfile();
+ }
+ }
+
+ public async Task GetProfile(){
+ if (crunInstance.Token?.access_token == null){
+ Console.Error.WriteLine("Missing Access Token");
+ return;
+ }
+
+ var request = HttpClientReq.CreateRequestMessage(Api.BetaProfile, HttpMethod.Get, true, true, null);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ var profileTemp = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
+
+ if (profileTemp != null){
+ crunInstance.Profile = profileTemp;
+ }
+ }
+ }
+
+ public async void LoginWithToken(){
+ if (crunInstance.Token?.refresh_token == null){
+ Console.WriteLine("Missing Refresh Token");
+ return;
+ }
+
+ var formData = new Dictionary{
+ { "refresh_token", crunInstance.Token.refresh_token },
+ { "grant_type", "refresh_token" },
+ { "scope", "offline_access" }
+ };
+ var requestContent = new FormUrlEncodedContent(formData);
+ requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+
+ var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){
+ Content = requestContent
+ };
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ JsonTokenToFileAndVariable(response.ResponseContent);
+ } else{
+ Console.WriteLine("Token Auth Failed");
+ }
+
+ if (crunInstance.Token?.refresh_token != null){
+ await GetProfile();
+ }
+
+ await GetCmsToken();
+ }
+
+ public async Task RefreshToken(bool needsToken){
+ if (crunInstance.Token?.access_token == null && crunInstance.Token?.refresh_token == null ||
+ crunInstance.Token.access_token != null && crunInstance.Token.refresh_token == null){
+ await AuthAnonymous();
+ } else{
+ if (!(DateTime.Now > crunInstance.Token.expires) && needsToken){
+ return;
+ }
+ }
+
+ var formData = new Dictionary{
+ { "refresh_token", crunInstance.Token?.refresh_token ?? string.Empty },
+ { "grant_type", "refresh_token" },
+ { "scope", "offline_access" }
+ };
+ var requestContent = new FormUrlEncodedContent(formData);
+ requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
+
+ var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){
+ Content = requestContent
+ };
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ JsonTokenToFileAndVariable(response.ResponseContent);
+ } else{
+ Console.WriteLine("Refresh Token Auth Failed");
+ }
+
+ await GetCmsToken();
+ }
+
+
+ public async Task GetCmsToken(){
+ if (crunInstance.Token?.access_token == null){
+ Console.WriteLine($"Missing Access Token: {crunInstance.Token?.access_token}");
+ return;
+ }
+
+ var request = HttpClientReq.CreateRequestMessage(Api.BetaCmsToken, HttpMethod.Get, true, true, null);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ crunInstance.CmsToken = JsonConvert.DeserializeObject(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
+ } else{
+ Console.WriteLine("CMS Token Auth Failed");
+ }
+ }
+
+ public async Task GetCmsData(){
+ if (crunInstance.CmsToken?.Cms == null){
+ Console.WriteLine("Missing CMS Token");
+ return;
+ }
+
+ UriBuilder uriBuilder = new UriBuilder(Api.BetaCms + crunInstance.CmsToken.Cms.Bucket + "/index?");
+
+ NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query);
+
+ query["preferred_audio_language"] = "ja-JP";
+ query["Policy"] = crunInstance.CmsToken.Cms.Policy;
+ query["Signature"] = crunInstance.CmsToken.Cms.Signature;
+ query["Key-Pair-Id"] = crunInstance.CmsToken.Cms.KeyPairId;
+
+ uriBuilder.Query = query.ToString();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.ToString());
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (response.IsOk){
+ Console.WriteLine(response.ResponseContent);
+ } else{
+ Console.WriteLine("Failed to Get CMS Index");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Downloader/CrEpisode.cs b/Downloader/CrEpisode.cs
new file mode 100644
index 0000000..1dc5710
--- /dev/null
+++ b/Downloader/CrEpisode.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Web;
+using CRD.Utils;
+using CRD.Utils.Structs;
+using Newtonsoft.Json;
+
+namespace CRD.Downloader;
+
+public class CrEpisode(Crunchyroll crunInstance){
+ public async Task ParseEpisodeById(string id,string locale){
+ if (crunInstance.CmsToken?.Cms == null){
+ Console.WriteLine("Missing CMS Access Token");
+ return null;
+ }
+
+ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
+
+ query["preferred_audio_language"] = "ja-JP";
+ query["locale"] = Languages.Locale2language(locale).CrLocale;
+
+ var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/episodes/{id}", HttpMethod.Get, true, true, query);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (!response.IsOk){
+ Console.WriteLine("Series Request Failed");
+ return null;
+ }
+
+ CrunchyEpisodeList epsidoe = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
+
+ if (epsidoe.Total < 1){
+ return null;
+ }
+
+ return epsidoe;
+ }
+
+
+ public CrunchySeriesList EpisodeData(CrunchyEpisodeList dlEpisodes){
+ bool serieshasversions = true;
+
+ Dictionary episodes = new Dictionary();
+
+ if (dlEpisodes.Data != null){
+ foreach (var episode in dlEpisodes.Data){
+
+ if (crunInstance.CrunOptions.History){
+ crunInstance.CrHistory.UpdateWithEpisode(episode);
+ }
+
+ // Prepare the episode array
+ EpisodeAndLanguage item;
+ var seasonIdentifier = !string.IsNullOrEmpty(episode.Identifier) ? episode.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}";
+ var episodeKey = $"{seasonIdentifier}E{episode.Episode ?? (episode.EpisodeNumber + "")}";
+
+ if (!episodes.ContainsKey(episodeKey)){
+ item = new EpisodeAndLanguage{
+ Items = new List(),
+ Langs = new List()
+ };
+ episodes[episodeKey] = item;
+ } else{
+ item = episodes[episodeKey];
+ }
+
+ if (episode.Versions != null){
+ foreach (var version in episode.Versions){
+ // Ensure there is only one of the same language
+ if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
+ // Push to arrays if there are no duplicates of the same language
+ item.Items.Add(episode);
+ item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale));
+ }
+ }
+ } else{
+ // Episode didn't have versions, mark it as such to be logged.
+ serieshasversions = false;
+ // Ensure there is only one of the same language
+ if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
+ // Push to arrays if there are no duplicates of the same language
+ item.Items.Add(episode);
+ item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale));
+ }
+ }
+ }
+ }
+
+ int specialIndex = 1;
+ int epIndex = 1;
+
+ var keys = new List(episodes.Keys); // Copying the keys to a new list to avoid modifying the collection while iterating.
+
+ foreach (var key in keys){
+ EpisodeAndLanguage item = episodes[key];
+ var isSpecial = !Regex.IsMatch(item.Items[0].Episode ?? string.Empty, @"^\d+$"); // Checking if the episode is not a number (i.e., special).
+ string newKey;
+ if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){
+ newKey = item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id);
+ } else{
+ newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}";
+ }
+
+ episodes.Remove(key);
+ episodes.Add(newKey, item);
+
+ if (isSpecial){
+ specialIndex++;
+ } else{
+ epIndex++;
+ }
+ }
+
+ var specials = episodes.Where(e => e.Key.StartsWith("SP")).ToList();
+ var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList();
+
+ // Combining and sorting episodes with normal first, then specials.
+ var sortedEpisodes = new Dictionary(normal.Concat(specials));
+
+ foreach (var kvp in sortedEpisodes){
+ var key = kvp.Key;
+ var item = kvp.Value;
+
+ var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
+ ?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
+
+ var title = item.Items[0].Title;
+ var seasonNumber = item.Items[0].SeasonNumber;
+
+ var languages = item.Items.Select((a, index) =>
+ $"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
+
+ Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
+ }
+
+ if (!serieshasversions){
+ Console.WriteLine("Couldn\'t find versions on some episodes, fell back to old method.");
+ }
+
+ CrunchySeriesList crunchySeriesList = new CrunchySeriesList();
+ crunchySeriesList.Data = sortedEpisodes;
+
+ crunchySeriesList.List = sortedEpisodes.Select(kvp => {
+ var key = kvp.Key;
+ var value = kvp.Value;
+ var images = (value.Items[0].Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } });
+ var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0);
+ return new Episode{
+ E = key.StartsWith("E") ? key.Substring(1) : key,
+ Lang = value.Langs.Select(a => a.Code).ToList(),
+ Name = value.Items[0].Title,
+ Season = value.Items[0].SeasonNumber.ToString(),
+ SeriesTitle = Regex.Replace(value.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(),
+ SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(),
+ EpisodeNum = value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?",
+ Id = value.Items[0].SeasonId,
+ Img = images[images.Count / 2].FirstOrDefault().Source,
+ Description = value.Items[0].Description,
+ Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds.
+ };
+ }).ToList();
+
+ return crunchySeriesList;
+ }
+
+ public Dictionary EpisodeMeta(Dictionary eps, List dubLang){
+ var ret = new Dictionary();
+
+
+ foreach (var kvp in eps){
+ var key = kvp.Key;
+ var episode = kvp.Value;
+
+ for (int index = 0; index < episode.Items.Count; index++){
+ var item = episode.Items[index];
+
+ if (!dubLang.Contains(episode.Langs[index].CrLocale))
+ continue;
+
+ item.HideSeasonTitle = true;
+ if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
+ item.SeasonTitle = item.SeriesTitle;
+ item.HideSeasonTitle = false;
+ item.HideSeasonNumber = true;
+ }
+
+ if (string.IsNullOrEmpty(item.SeasonTitle) && string.IsNullOrEmpty(item.SeriesTitle)){
+ item.SeasonTitle = "NO_TITLE";
+ item.SeriesTitle = "NO_TITLE";
+ }
+
+ var epNum = key.StartsWith('E') ? key[1..] : key;
+ var images = (item.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } });
+
+ Regex dubPattern = new Regex(@"\(\w+ Dub\)");
+
+ var epMeta = new CrunchyEpMeta();
+ epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
+ epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
+ epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
+ epMeta.EpisodeNumber = item.Episode;
+ epMeta.EpisodeTitle = item.Title;
+ epMeta.SeasonId = item.SeasonId;
+ epMeta.Season = item.SeasonNumber;
+ epMeta.ShowId = item.SeriesId;
+ epMeta.AbsolutEpisodeNumberE = epNum;
+ epMeta.Image = images[images.Count / 2].FirstOrDefault().Source;
+ epMeta.DownloadProgress = new DownloadProgress(){
+ IsDownloading = false,
+ Done = false,
+ Error = false,
+ Percent = 0,
+ Time = 0,
+ DownloadSpeed = 0
+ };
+
+ var epMetaData = epMeta.Data[0];
+ if (!string.IsNullOrEmpty(item.StreamsLink)){
+ epMetaData.Playback = item.StreamsLink;
+ if (string.IsNullOrEmpty(item.Playback)){
+ item.Playback = item.StreamsLink;
+ }
+ }
+
+ if (ret.TryGetValue(key, out var epMe)){
+ epMetaData.Lang = episode.Langs[index];
+ epMe.Data?.Add(epMetaData);
+ } else{
+ epMetaData.Lang = episode.Langs[index];
+ epMeta.Data[0] = epMetaData;
+ ret.Add(key, epMeta);
+ }
+
+
+ // show ep
+ item.SeqId = epNum;
+ }
+ }
+
+
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/Downloader/CrSeries.cs b/Downloader/CrSeries.cs
new file mode 100644
index 0000000..faa7352
--- /dev/null
+++ b/Downloader/CrSeries.cs
@@ -0,0 +1,406 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Web;
+using CRD.Utils;
+using CRD.Utils.Structs;
+using Newtonsoft.Json;
+
+namespace CRD.Downloader;
+
+public class CrSeries(Crunchyroll crunInstance){
+ public async Task> DownloadFromSeriesId(string id, CrunchyMultiDownload data){
+ var series = await ListSeriesId(id, "" ,data);
+
+ if (series != null){
+ var selected = ItemSelectMultiDub(series.Value.Data, data.DubLang, data.But, data.AllEpisodes, data.E);
+
+ foreach (var crunchyEpMeta in selected.Values){
+ if (crunchyEpMeta.Data == null) continue;
+ var languages = crunchyEpMeta.Data.Select((a) =>
+ $" {a.Lang?.Name ?? "Unknown Language"}");
+
+ Console.WriteLine($"[S{crunchyEpMeta.Season}E{crunchyEpMeta.EpisodeNumber} - {crunchyEpMeta.EpisodeTitle} [{string.Join(", ", languages)}]");
+ }
+
+ return selected.Values.ToList();
+ }
+
+ return new List();
+ }
+
+ public Dictionary ItemSelectMultiDub(Dictionary eps, List dubLang, bool? but, bool? all, List? e){
+ var ret = new Dictionary();
+
+
+ foreach (var kvp in eps){
+ var key = kvp.Key;
+ var episode = kvp.Value;
+
+ for (int index = 0; index < episode.Items.Count; index++){
+ var item = episode.Items[index];
+
+ if (!dubLang.Contains(episode.Langs[index].CrLocale))
+ continue;
+
+ item.HideSeasonTitle = true;
+ if (string.IsNullOrEmpty(item.SeasonTitle) && !string.IsNullOrEmpty(item.SeriesTitle)){
+ item.SeasonTitle = item.SeriesTitle;
+ item.HideSeasonTitle = false;
+ item.HideSeasonNumber = true;
+ }
+
+ if (string.IsNullOrEmpty(item.SeasonTitle) && string.IsNullOrEmpty(item.SeriesTitle)){
+ item.SeasonTitle = "NO_TITLE";
+ item.SeriesTitle = "NO_TITLE";
+ }
+
+ var epNum = key.StartsWith('E') ? key[1..] : key;
+ var images = (item.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } });
+
+ Regex dubPattern = new Regex(@"\(\w+ Dub\)");
+
+ var epMeta = new CrunchyEpMeta();
+ epMeta.Data = new List{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
+ epMeta.SeriesTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episode.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
+ epMeta.SeasonTitle = episode.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episode.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
+ epMeta.EpisodeNumber = item.Episode;
+ epMeta.EpisodeTitle = item.Title;
+ epMeta.SeasonId = item.SeasonId;
+ epMeta.Season = item.SeasonNumber;
+ epMeta.ShowId = item.SeriesId;
+ epMeta.AbsolutEpisodeNumberE = epNum;
+ epMeta.Image = images[images.Count / 2].FirstOrDefault().Source;
+ epMeta.DownloadProgress = new DownloadProgress(){
+ IsDownloading = false,
+ Done = false,
+ Percent = 0,
+ Time = 0,
+ DownloadSpeed = 0
+ };
+
+
+
+ var epMetaData = epMeta.Data[0];
+ if (!string.IsNullOrEmpty(item.StreamsLink)){
+ epMetaData.Playback = item.StreamsLink;
+ if (string.IsNullOrEmpty(item.Playback)){
+ item.Playback = item.StreamsLink;
+ }
+ }
+
+ if (all is true || e != null && e.Contains(epNum)){
+ if (ret.TryGetValue(key, out var epMe)){
+ epMetaData.Lang = episode.Langs[index];
+ epMe.Data?.Add(epMetaData);
+ } else{
+ epMetaData.Lang = episode.Langs[index];
+ epMeta.Data[0] = epMetaData;
+ ret.Add(key, epMeta);
+ }
+ }
+
+
+ // show ep
+ item.SeqId = epNum;
+ }
+ }
+
+
+ return ret;
+ }
+
+
+ public async Task ListSeriesId(string id,string Locale, CrunchyMultiDownload? data){
+ await crunInstance.CrAuth.RefreshToken(true);
+
+ bool serieshasversions = true;
+
+ CrSeriesSearch? parsedSeries = await ParseSeriesById(id,Locale); // one piece - GRMG8ZQZR
+
+ if (parsedSeries == null){
+ Console.WriteLine("Parse Data Invalid");
+ return null;
+ }
+
+ var result = ParseSeriesResult(parsedSeries);
+ Dictionary episodes = new Dictionary();
+
+
+ foreach (int season in result.Keys){
+ foreach (var key in result[season].Keys){
+ var s = result[season][key];
+ if (data?.S != null && s.Id != data.Value.S) continue;
+ int fallbackIndex = 0;
+ var seasonData = await GetSeasonDataById(s);
+ if (seasonData.Data != null){
+
+ if (crunInstance.CrunOptions.History){
+ crunInstance.CrHistory.UpdateWithSeasonData(seasonData);
+ }
+
+ foreach (var episode in seasonData.Data){
+ // Prepare the episode array
+ EpisodeAndLanguage item;
+
+
+ string episodeNum = (episode.Episode != String.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}")) ?? string.Empty;
+
+ var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) ? s.Identifier.Split('|')[1] : $"S{episode.SeasonNumber}";
+ var episodeKey = $"{seasonIdentifier}E{episodeNum}";
+
+ if (!episodes.ContainsKey(episodeKey)){
+ item = new EpisodeAndLanguage{
+ Items = new List(),
+ Langs = new List()
+ };
+ episodes[episodeKey] = item;
+ } else{
+ item = episodes[episodeKey];
+ }
+
+ if (episode.Versions != null){
+ foreach (var version in episode.Versions){
+ // Ensure there is only one of the same language
+ if (item.Langs.All(a => a.CrLocale != version.AudioLocale)){
+ // Push to arrays if there are no duplicates of the same language
+ item.Items.Add(episode);
+ item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == version.AudioLocale));
+ }
+ }
+ } else{
+ // Episode didn't have versions, mark it as such to be logged.
+ serieshasversions = false;
+ // Ensure there is only one of the same language
+ if (item.Langs.All(a => a.CrLocale != episode.AudioLocale)){
+ // Push to arrays if there are no duplicates of the same language
+ item.Items.Add(episode);
+ item.Langs.Add(Array.Find(Languages.languages, a => a.CrLocale == episode.AudioLocale));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ int specialIndex = 1;
+ int epIndex = 1;
+
+ var keys = new List(episodes.Keys); // Copying the keys to a new list to avoid modifying the collection while iterating.
+
+ foreach (var key in keys){
+ EpisodeAndLanguage item = episodes[key];
+ var episode = item.Items[0].Episode;
+ var isSpecial = episode != null && !Regex.IsMatch(episode, @"^\d+$"); // Checking if the episode is not a number (i.e., special).
+ // var newKey = $"{(isSpecial ? 'S' : 'E')}{(isSpecial ? specialIndex : epIndex).ToString()}";
+
+ string newKey;
+ if (isSpecial && !string.IsNullOrEmpty(item.Items[0].Episode)){
+ newKey = item.Items[0].Episode ?? "SP" + (specialIndex + " " + item.Items[0].Id);
+ } else{
+ newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + item.Items[0].Id) : epIndex + "")}";
+ }
+
+ episodes.Remove(key);
+ episodes.Add(newKey, item);
+
+ if (isSpecial){
+ specialIndex++;
+ } else{
+ epIndex++;
+ }
+ }
+
+ var specials = episodes.Where(e => e.Key.StartsWith("S")).ToList();
+ var normal = episodes.Where(e => e.Key.StartsWith("E")).ToList();
+
+ // Combining and sorting episodes with normal first, then specials.
+ var sortedEpisodes = new Dictionary(normal.Concat(specials));
+
+ foreach (var kvp in sortedEpisodes){
+ var key = kvp.Key;
+ var item = kvp.Value;
+
+ var seasonTitle = item.Items.FirstOrDefault(a => !Regex.IsMatch(a.SeasonTitle, @"\(\w+ Dub\)")).SeasonTitle
+ ?? Regex.Replace(item.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
+
+ var title = item.Items[0].Title;
+ var seasonNumber = item.Items[0].SeasonNumber;
+
+ var languages = item.Items.Select((a, index) =>
+ $"{(a.IsPremiumOnly ? "+ " : "")}{item.Langs.ElementAtOrDefault(index).Name ?? "Unknown"}").ToArray(); //☆
+
+ Console.WriteLine($"[{key}] {seasonTitle} - Season {seasonNumber} - {title} [{string.Join(", ", languages)}]");
+ }
+
+ if (!serieshasversions){
+ Console.WriteLine("Couldn\'t find versions on some episodes, fell back to old method.");
+ }
+
+ CrunchySeriesList crunchySeriesList = new CrunchySeriesList();
+ crunchySeriesList.Data = sortedEpisodes;
+
+ crunchySeriesList.List = sortedEpisodes.Select(kvp => {
+ var key = kvp.Key;
+ var value = kvp.Value;
+ var images = (value.Items[0].Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } });
+ var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0);
+ return new Episode{
+ E = key.StartsWith("E") ? key.Substring(1) : key,
+ Lang = value.Langs.Select(a => a.Code).ToList(),
+ Name = value.Items[0].Title,
+ Season = value.Items[0].SeasonNumber.ToString(),
+ SeriesTitle = Regex.Replace(value.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(),
+ SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(),
+ EpisodeNum = value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?",
+ Id = value.Items[0].SeasonId,
+ Img = images[images.Count / 2].FirstOrDefault().Source,
+ Description = value.Items[0].Description,
+ Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds.
+ };
+ }).ToList();
+
+ return crunchySeriesList;
+ }
+
+ public async Task GetSeasonDataById(SeriesSearchItem item, bool log = false){
+ CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List(), Total = 0, Meta = new Meta() };
+
+ if (crunInstance.CmsToken?.Cms == null){
+ Console.WriteLine("Missing CMS Token");
+ return episodeList;
+ }
+
+ if (log){
+ var showRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{item.Id}?preferred_audio_language=ja-JP", HttpMethod.Get, true, true, null);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(showRequest);
+
+ if (!response.IsOk){
+ Console.WriteLine("Show Request FAILED!");
+ } else{
+ Console.WriteLine(response.ResponseContent);
+ }
+ }
+
+ //TODO
+
+ var episodeRequest = new HttpRequestMessage(HttpMethod.Get, $"{Api.Cms}/seasons/{item.Id}/episodes?preferred_audio_language=ja-JP");
+
+ episodeRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", crunInstance.Token?.access_token);
+
+ var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest);
+
+ if (!episodeRequestResponse.IsOk){
+ Console.WriteLine($"Episode List Request FAILED! uri: {episodeRequest.RequestUri}");
+ } else{
+ episodeList = Helpers.Deserialize(episodeRequestResponse.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
+ }
+
+ if (episodeList.Total < 1){
+ Console.WriteLine("Season is empty!");
+ }
+
+ return episodeList;
+ }
+
+ public Dictionary> ParseSeriesResult(CrSeriesSearch seasonsList){
+ var ret = new Dictionary>();
+ int i = 0;
+
+ 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();
+ }
+
+ 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 ParseSeriesById(string id,string? locale){
+ if (crunInstance.CmsToken?.Cms == null){
+ Console.WriteLine("Missing CMS Access Token");
+ return null;
+ }
+
+ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
+
+ query["preferred_audio_language"] = "ja-JP";
+ if (!string.IsNullOrEmpty(locale)){
+ query["locale"] = Languages.Locale2language(locale).CrLocale;
+ }
+
+
+ var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (!response.IsOk){
+ Console.WriteLine("Series Request Failed");
+ return null;
+ }
+
+
+ CrSeriesSearch? seasonsList = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
+
+ if (seasonsList == null || seasonsList.Total < 1){
+ return null;
+ }
+
+ return seasonsList;
+ }
+
+ public async Task SeriesById(string id){
+ if (crunInstance.CmsToken?.Cms == null){
+ Console.WriteLine("Missing CMS Access Token");
+ return null;
+ }
+
+ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
+
+ query["preferred_audio_language"] = "ja-JP";
+
+ var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}", HttpMethod.Get, true, true, query);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ if (!response.IsOk){
+ Console.WriteLine("Series Request Failed");
+ return null;
+ }
+
+
+ CrSeriesBase? series = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
+
+ if (series == null || series.Total < 1){
+ return null;
+ }
+
+ return series;
+ }
+
+}
\ No newline at end of file
diff --git a/Downloader/Crunchyroll.cs b/Downloader/Crunchyroll.cs
new file mode 100644
index 0000000..a96b7a5
--- /dev/null
+++ b/Downloader/Crunchyroll.cs
@@ -0,0 +1,1617 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Web;
+using Avalonia.Media;
+using CRD.Utils;
+using CRD.Utils.CustomList;
+using CRD.Utils.DRM;
+using CRD.Utils.HLS;
+using CRD.Utils.Muxing;
+using CRD.Utils.Structs;
+using CRD.ViewModels;
+using CRD.Views;
+using HtmlAgilityPack;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using ReactiveUI;
+using LanguageItem = CRD.Utils.Structs.LanguageItem;
+
+namespace CRD.Downloader;
+
+public class Crunchyroll{
+ public CrToken? Token;
+ public CrCmsToken? CmsToken;
+ private readonly string _api = "web"; //web | android
+
+ public CrProfile Profile = new();
+ public CrDownloadOptions CrunOptions;
+
+ #region Download Variables
+
+ public RefreshableObservableCollection Queue = new RefreshableObservableCollection();
+ public ObservableCollection DownloadItemModels = new ObservableCollection();
+ public int ActiveDownloads;
+ public bool AutoDownload = false;
+
+ #endregion
+
+
+ #region Calendar Variables
+
+ private Dictionary calendar = new();
+ private Dictionary calendarLanguage = new();
+
+ #endregion
+
+
+ #region History Variables
+
+ public ObservableCollection HistoryList = new();
+
+ public HistorySeries SelectedSeries = new HistorySeries{
+ Seasons =[]
+ };
+
+ #endregion
+
+ public string DefaultLocale = "en";
+
+ public JsonSerializerSettings? SettingsJsonSerializerSettings = new(){
+ NullValueHandling = NullValueHandling.Ignore,
+ };
+
+ private Widevine _widevine = Widevine.Instance;
+
+ public CrAuth CrAuth;
+ public CrEpisode CrEpisode;
+ public CrSeries CrSeries;
+ public History CrHistory;
+
+ #region Singelton
+
+ private static Crunchyroll? _instance;
+ private static readonly object Padlock = new();
+
+ public static Crunchyroll Instance{
+ get{
+ if (_instance == null){
+ lock (Padlock){
+ if (_instance == null){
+ _instance = new Crunchyroll();
+ }
+ }
+ }
+
+ return _instance;
+ }
+ }
+
+ #endregion
+
+ public async Task Init(){
+ _widevine = Widevine.Instance;
+
+ CrAuth = new CrAuth(Instance);
+ CrEpisode = new CrEpisode(Instance);
+ CrSeries = new CrSeries(Instance);
+ CrHistory = new History(Instance);
+
+ Profile = new CrProfile{
+ Username = "???",
+ Avatar = "003-cr-hime-excited.png",
+ PreferredContentAudioLanguage = "ja-JP",
+ PreferredContentSubtitleLanguage = "de-DE"
+ };
+
+
+ if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
+ Token = CfgManager.DeserializeFromFile(CfgManager.PathCrToken);
+ CrAuth.LoginWithToken();
+ } else{
+ await CrAuth.AuthAnonymous();
+ }
+
+ Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}");
+
+ CrunOptions = new CrDownloadOptions();
+
+ CrunOptions.Chapters = true;
+ CrunOptions.Hslang = "none";
+ CrunOptions.Force = "Y";
+ CrunOptions.FileName = "${showTitle} - S${season}E${episode} [${height}p]";
+ CrunOptions.Partsize = 10;
+ CrunOptions.NoSubs = false;
+ CrunOptions.DlSubs = new List{ "de-DE" };
+ CrunOptions.Skipmux = false;
+ CrunOptions.MkvmergeOptions = new List{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" };
+ CrunOptions.DefaultAudio = Languages.FindLang("ja-JP");
+ CrunOptions.DefaultSub = Languages.FindLang("de-DE");
+ CrunOptions.CcTag = "cc";
+ CrunOptions.FsRetryTime = 5;
+ CrunOptions.Numbers = 2;
+ CrunOptions.Timeout = 15000;
+ CrunOptions.DubLang = new List(){ "ja-JP" };
+ CrunOptions.SimultaneousDownloads = 2;
+ CrunOptions.AccentColor = Colors.SlateBlue.ToString();
+ CrunOptions.Theme = "System";
+ CrunOptions.SelectedCalendarLanguage = "de";
+
+ CrunOptions.History = true;
+
+ CfgManager.UpdateSettingsFromFile();
+
+ if (CrunOptions.History){
+ if (File.Exists(CfgManager.PathCrHistory)){
+ HistoryList = JsonConvert.DeserializeObject>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
+ }
+ }
+
+
+ calendarLanguage = new(){
+ { "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
+ { "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
+ { "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" },
+ { "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" },
+ { "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" },
+ { "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" },
+ { "de", "https://www.crunchyroll.com/de/simulcastcalendar" },
+ { "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" },
+ { "it", "https://www.crunchyroll.com/it/simulcastcalendar" },
+ { "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" },
+ { "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" },
+ };
+ }
+
+ // public async void TestMethode(){
+ // // One Pice - GRMG8ZQZR
+ // // Studio - G9VHN9QWQ
+ // var episodesMeta = await DownloadFromSeriesId("G9VHN9QWQ", new CrunchyMultiDownload(Crunchy.Instance.CrunOptions.dubLang, true));
+ //
+ //
+ // foreach (var crunchyEpMeta in episodesMeta){
+ // await DownloadEpisode(crunchyEpMeta, CrunOptions, false);
+ // }
+ // }
+
+ public async Task GetCalendarForDate(string weeksMondayDate, bool forceUpdate){
+ if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){
+ return forDate;
+ }
+
+ var request = HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunOptions.SelectedCalendarLanguage ?? "de"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, true, true, null);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(request);
+
+ CalendarWeek week = new CalendarWeek();
+ week.CalendarDays = new List();
+
+ // Load the HTML content from a file
+ HtmlDocument doc = new HtmlDocument();
+ doc.LoadHtml(WebUtility.HtmlDecode(response.ResponseContent));
+
+ // Select each 'li' element with class 'day'
+ var dayNodes = doc.DocumentNode.SelectNodes("//li[contains(@class, 'day')]");
+
+ if (dayNodes != null){
+ foreach (var day in dayNodes){
+ // Extract the date and day name
+ var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date");
+ DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind);
+
+ if (week.FirstDayOfWeek == null){
+ week.FirstDayOfWeek = dayDateTime;
+ week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd");
+ }
+
+ var dayName = day.SelectSingleNode(".//h1[@class='day-name']/time")?.InnerText.Trim();
+
+ // Console.WriteLine($"Day: {dayName}, Date: {date}");
+
+ CalendarDay calDay = new CalendarDay();
+
+ calDay.CalendarEpisodes = new List();
+ calDay.DayName = dayName;
+ calDay.DateTime = dayDateTime;
+
+ // Iterate through each episode listed under this day
+ var episodes = day.SelectNodes(".//article[contains(@class, 'release')]");
+ if (episodes != null){
+ foreach (var episode in episodes){
+ var episodeTimeStr = episode.SelectSingleNode(".//time[contains(@class, 'available-time')]")?.GetAttributeValue("datetime", null);
+ DateTime episodeTime = DateTime.Parse(episodeTimeStr, null, DateTimeStyles.RoundtripKind);
+ var hasPassed = DateTime.Now > episodeTime;
+
+ var episodeName = episode.SelectSingleNode(".//h1[contains(@class, 'episode-name')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim();
+ var seasonLink = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.GetAttributeValue("href", "No link");
+ var episodeLink = episode.SelectSingleNode(".//a[contains(@class, 'available-episode-link')]")?.GetAttributeValue("href", "No link");
+ var thumbnailUrl = episode.SelectSingleNode(".//img[contains(@class, 'thumbnail')]")?.GetAttributeValue("src", "No image");
+ var isPremiumOnly = episode.SelectSingleNode(".//svg[contains(@class, 'premium-flag')]") != null;
+ var seasonName = episode.SelectSingleNode(".//a[contains(@class, 'js-season-name-link')]")?.SelectSingleNode(".//cite[@itemprop='name']")?.InnerText.Trim();
+ var episodeNumber = episode.SelectSingleNode(".//meta[contains(@itemprop, 'episodeNumber')]")?.GetAttributeValue("content", "?");
+
+ // Console.WriteLine($" Time: {episodeTime} (Has Passed: {hasPassed}), Episode: {episodeName}");
+ // Console.WriteLine($" Season Link: {seasonLink}");
+ // Console.WriteLine($" Episode Link: {episodeLink}");
+ // Console.WriteLine($" Thumbnail URL: {thumbnailUrl}");
+
+ CalendarEpisode calEpisode = new CalendarEpisode();
+
+ calEpisode.DateTime = episodeTime;
+ calEpisode.HasPassed = hasPassed;
+ calEpisode.EpisodeName = episodeName;
+ calEpisode.SeasonUrl = seasonLink;
+ calEpisode.EpisodeUrl = episodeLink;
+ calEpisode.ThumbnailUrl = thumbnailUrl;
+ calEpisode.IsPremiumOnly = isPremiumOnly;
+ calEpisode.SeasonName = seasonName;
+ calEpisode.EpisodeNumber = episodeNumber;
+
+ calDay.CalendarEpisodes.Add(calEpisode);
+ }
+ }
+
+ week.CalendarDays.Add(calDay);
+ // Console.WriteLine();
+ }
+ } else{
+ Console.WriteLine("No days found in the HTML document.");
+ }
+
+ calendar[weeksMondayDate] = week;
+
+
+ return week;
+ }
+
+ public async void AddEpisodeToQue(string epId, string locale, List dubLang){
+ await CrAuth.RefreshToken(true);
+
+ var episodeL = await CrEpisode.ParseEpisodeById(epId, locale);
+
+
+ if (episodeL != null){
+ if (episodeL.Value.Data != null && episodeL.Value.Data.First().IsPremiumOnly && Profile.Username == "???"){
+ MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode - try to login first", ToastType.Error, 3));
+ return;
+ }
+
+ var sList = CrEpisode.EpisodeData((CrunchyEpisodeList)episodeL);
+ var selected = CrEpisode.EpisodeMeta(sList.Data, dubLang);
+ var metas = selected.Values.ToList();
+ foreach (var crunchyEpMeta in metas){
+ Queue.Add(crunchyEpMeta);
+ }
+ Console.WriteLine("Added Episode to Queue");
+ MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1));
+ }
+ }
+
+ public void AddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){
+ var selected = CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.But, data.AllEpisodes, data.E);
+
+ foreach (var crunchyEpMeta in selected.Values.ToList()){
+ Queue.Add(crunchyEpMeta);
+ }
+ }
+
+
+ public async Task DownloadEpisode(CrunchyEpMeta data, CrDownloadOptions options, bool? isSeries){
+ ActiveDownloads++;
+
+ data.DownloadProgress = new DownloadProgress(){
+ IsDownloading = true,
+ Error = false,
+ Percent = 0,
+ Time = 0,
+ DownloadSpeed = 0,
+ Doing = "Starting"
+ };
+ Queue.Refresh();
+ var res = await DownloadMediaList(data, options);
+
+ if (res.Error){
+ ActiveDownloads--;
+ data.DownloadProgress = new DownloadProgress(){
+ IsDownloading = false,
+ Error = true,
+ Percent = 100,
+ Time = 0,
+ DownloadSpeed = 0,
+ Doing = "Download Error"
+ };
+ Queue.Refresh();
+ return false;
+ }
+
+ if (options.Skipmux == false){
+ data.DownloadProgress = new DownloadProgress(){
+ IsDownloading = true,
+ Percent = 100,
+ Time = 0,
+ DownloadSpeed = 0,
+ Doing = "Muxing"
+ };
+
+ Queue.Refresh();
+
+ await MuxStreams(res.Data,
+ new CrunchyMuxOptions{
+ FfmpegOptions = options.FfmpegOptions,
+ SkipSubMux = options.Skipmux,
+ Output = res.FileName,
+ Mp4 = options.Mp4,
+ VideoTitle = options.VideoTitle,
+ Novids = options.Novids,
+ NoCleanup = options.Nocleanup,
+ DefaultAudio = options.DefaultAudio,
+ DefaultSub = options.DefaultSub,
+ MkvmergeOptions = options.MkvmergeOptions,
+ ForceMuxer = options.Force,
+ SyncTiming = options.SyncTiming,
+ CcTag = options.CcTag,
+ KeepAllVideos = false
+ },
+ res.FileName);
+
+ data.DownloadProgress = new DownloadProgress(){
+ IsDownloading = true,
+ Done = true,
+ Percent = 100,
+ Time = 0,
+ DownloadSpeed = 0,
+ Doing = "Done"
+ };
+
+ Queue.Refresh();
+ } else{
+ Console.WriteLine("Skipping mux");
+ }
+
+ ActiveDownloads--;
+
+ if (CrunOptions.History && data.Data != null && data.Data.Count > 0){
+ CrHistory.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId);
+ }
+
+
+ return true;
+ }
+
+ private async Task MuxStreams(List data, CrunchyMuxOptions options, string filename){
+ var hasAudioStreams = false;
+
+ var muxToMp3 = false;
+
+ if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
+ if (data.FindAll(a => a.Type == DownloadMediaType.Audio).Count > 0){
+ Console.WriteLine("Mux to MP3");
+ muxToMp3 = true;
+ } else{
+ Console.WriteLine("Skip muxing since no videos are downloaded");
+ return;
+ }
+ }
+
+ if (data.Any(a => a.Type == DownloadMediaType.Audio)){
+ hasAudioStreams = true;
+ }
+
+ var subs = data.Where(a => a.Type == DownloadMediaType.Subtitle).ToList();
+ var subsList = new List();
+
+ foreach (var downloadedMedia in subs){
+ var subt = new SubtitleFonts();
+ subt.Language = downloadedMedia.Language;
+ subt.Fonts = downloadedMedia.Fonts;
+ subsList.Add(subt);
+ }
+
+ var merger = new Merger(new MergerOptions{
+ OnlyVid = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List(),
+ SkipSubMux = options.SkipSubMux,
+ OnlyAudio = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List(),
+ Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
+ Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc, Signs = a.Signs }).ToList(),
+ Simul = false,
+ KeepAllVideos = options.KeepAllVideos,
+ Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined
+ VideoAndAudio = hasAudioStreams ? new List() : data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
+ Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
+ VideoTitle = options.VideoTitle,
+ Options = new MuxOptions(){
+ ffmpeg = options.FfmpegOptions,
+ mkvmerge = options.MkvmergeOptions
+ },
+ Defaults = new Defaults(){
+ Audio = options.DefaultAudio,
+ Sub = options.DefaultSub
+ },
+ CcTag = options.CcTag,
+ mp3 = muxToMp3
+ });
+
+ if (!File.Exists(CfgManager.PathFFMPEG)){
+ Console.WriteLine("FFmpeg not found");
+ }
+
+ if (!File.Exists(CfgManager.PathMKVMERGE)){
+ Console.WriteLine("MKVmerge not found");
+ }
+
+ bool isMuxed;
+
+ // if (options.SyncTiming){
+ // await Merger.CreateDelays();
+ // }
+
+ if (!options.Mp4 && !muxToMp3){
+ await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE);
+ isMuxed = true;
+ } else{
+ await merger.Merge("ffmpeg", CfgManager.PathFFMPEG);
+ isMuxed = true;
+ }
+
+ if (isMuxed && options.NoCleanup == false){
+ merger.CleanUp();
+ }
+ }
+
+ private async Task DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
+ if (CmsToken?.Cms == null){
+ Console.WriteLine("Missing CMS Token");
+ MainWindow.ShowError("Missing CMS Token - are you signed in?");
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ if (Profile.Username == "???"){
+ MainWindow.ShowError("User Account not recognized - are you signed in?");
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ if (!File.Exists(CfgManager.PathFFMPEG)){
+ Console.Error.WriteLine("Missing ffmpeg");
+ MainWindow.ShowError("FFmpeg not found");
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ string mediaName = $"{data.SeasonTitle} - {data.EpisodeNumber} - {data.EpisodeTitle}";
+ string fileName = "";
+ var variables = new List();
+
+ List files = new List();
+
+ if (data.Data != null && data.Data.All(a => a.Playback == null)){
+ Console.WriteLine("Video not available!");
+ MainWindow.ShowError("No Video Data found");
+ return new DownloadResponse{
+ Data = files,
+ Error = true,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown"
+ };
+ }
+
+ bool dlFailed = false;
+ bool dlVideoOnce = false;
+
+ if (data.Data != null)
+ foreach (CrunchyEpMetaData epMeta in data.Data){
+ Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
+
+ string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId);
+
+ await CrAuth.RefreshToken(true);
+
+ EpisodeVersion currentVersion = new EpisodeVersion();
+ EpisodeVersion primaryVersion = new EpisodeVersion();
+ bool isPrimary = epMeta.IsSubbed;
+
+ //Get Media GUID
+ string mediaId = epMeta.MediaId;
+ string mediaGuid = currentMediaId;
+ if (epMeta.Versions != null){
+ if (epMeta.Lang != null){
+ currentVersion = epMeta.Versions.Find(a => a.AudioLocale == epMeta.Lang?.CrLocale);
+ } else if (options.DubLang.Count == 1){
+ LanguageItem lang = Array.Find(Languages.languages, a => a.CrLocale == options.DubLang[0]);
+ currentVersion = epMeta.Versions.Find(a => a.AudioLocale == lang.CrLocale);
+ } else if (epMeta.Versions.Count == 1){
+ currentVersion = epMeta.Versions[0];
+ }
+
+ if (currentVersion.MediaGuid == null){
+ Console.WriteLine("Selected language not found in versions.");
+ MainWindow.ShowError("Selected language not found");
+ continue;
+ }
+
+ isPrimary = currentVersion.Original;
+ mediaId = currentVersion.MediaGuid;
+ mediaGuid = currentVersion.Guid;
+
+ if (!isPrimary){
+ primaryVersion = epMeta.Versions.Find(a => a.Original);
+ } else{
+ primaryVersion = currentVersion;
+ }
+ }
+
+ if (mediaId.Contains(':')){
+ mediaId = mediaId.Split(':')[1];
+ }
+
+ if (mediaGuid.Contains(':')){
+ mediaGuid = mediaGuid.Split(':')[1];
+ }
+
+ Console.WriteLine("MediaGuid: " + mediaId);
+
+ #region Chapters
+
+ List compiledChapters = new List();
+
+ if (options.Chapters){
+ await ParseChapters(primaryVersion.Guid, compiledChapters);
+ }
+
+ #endregion
+
+
+ var fetchPlaybackData = await FetchPlaybackData(mediaId, epMeta);
+
+ if (!fetchPlaybackData.IsOk){
+ MainWindow.ShowError("Couldn't get Playback Data");
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ var pbData = fetchPlaybackData.pbData;
+
+
+ #region NonDrmRequest
+
+ await FetchNoDrmPlaybackData(mediaGuid, pbData);
+
+ #endregion
+
+
+ List hsLangs = new List();
+ var pbStreams = pbData.Data?[0];
+ var streams = new List();
+
+ variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
+ variables.Add(new Variable("episode", (int.TryParse(data.EpisodeNumber, out int episodeNum) ? (object)episodeNum : data.AbsolutEpisodeNumberE) ?? string.Empty, false));
+ variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
+ variables.Add(new Variable("showTitle", data.SeasonTitle ?? string.Empty, true));
+ variables.Add(new Variable("season", data.Season ?? 0, false));
+
+ if (pbStreams?.Keys != null){
+ foreach (var key in pbStreams.Keys){
+ if ((key.Contains("hls") || key.Contains("dash")) &&
+ !(key.Contains("hls") && key.Contains("drm")) &&
+ !((!_widevine.canDecrypt || !File.Exists(CfgManager.PathMP4Decrypt)) && key.Contains("drm")) &&
+ !key.Contains("trailer")){
+ var pb = pbStreams[key].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);
+ }
+
+ return new StreamDetailsPop{
+ Url = v.Value.Url,
+ HardsubLocale = v.Value.HardsubLocale,
+ HardsubLang = v.Value.HardsubLang,
+ AudioLang = v.Value.AudioLang,
+ Type = v.Value.Type,
+ Format = key,
+ };
+ }).ToList();
+
+ streams.AddRange(pb);
+ }
+ }
+
+ if (streams.Count < 1){
+ Console.WriteLine("No full streams found!");
+ MainWindow.ShowError("No streams found");
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ var audDub = "";
+ if (pbData.Meta != null){
+ audDub = Languages.FindLang(Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue())).Code;
+ }
+
+ hsLangs = Languages.SortTags(hsLangs);
+
+ streams = streams.Select(s => {
+ s.AudioLang = audDub;
+ s.HardsubLang = string.IsNullOrEmpty(s.HardsubLang) ? "-" : s.HardsubLang;
+ s.Type = $"{s.Format}/{s.AudioLang}/{s.HardsubLang}";
+ return s;
+ }).ToList();
+
+ streams.Sort((a, b) => String.CompareOrdinal(a.Type, b.Type));
+
+ 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();
+ } else{
+ Console.WriteLine($"Selected stream with {Languages.Locale2language(options.Hslang).Language} hardsubs not available");
+ if (hsLangs.Count > 0){
+ Console.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
+ }
+
+ dlFailed = true;
+ }
+ } else{
+ streams = streams.Where((s) => {
+ if (s.HardsubLang != "-"){
+ return false;
+ }
+
+ return true;
+ }).ToList();
+
+ if (streams.Count < 1){
+ Console.WriteLine("Raw streams not available!");
+ if (hsLangs.Count > 0){
+ Console.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
+ }
+
+ dlFailed = true;
+ }
+
+ Console.WriteLine("Selecting raw stream");
+ }
+
+ StreamDetailsPop? curStream = null;
+ if (!dlFailed){
+ // Validate or adjust options.kstream
+ options.Kstream = options.Kstream >= 1 && options.Kstream <= streams.Count
+ ? options.Kstream
+ : 1;
+
+ for (int i = 0; i < streams.Count; i++){
+ string isSelected = options.Kstream == i + 1 ? "✓" : " ";
+ Console.WriteLine($"Full stream found! ({isSelected}{i + 1}: {streams[i].Type})");
+ }
+
+ Console.WriteLine("Downloading video...");
+ curStream = streams[options.Kstream - 1];
+
+ Console.WriteLine($"Playlists URL: {curStream.Url} ({curStream.Type})");
+ }
+
+ string tsFile = "";
+
+ 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 streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq);
+
+ if (!streamPlaylistsReqResponse.IsOk){
+ dlFailed = true;
+ }
+
+ if (dlFailed){
+ Console.WriteLine($"CAN\'T FETCH VIDEO PLAYLISTS!");
+ } else{
+ if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
+ var match = Regex.Match(curStream.Url ?? string.Empty, @"(.*\.urlset\/)");
+ var matchedUrl = match.Success ? match.Value : null;
+ //Parse MPD Playlists
+ var crLocal = "";
+ if (pbData.Meta != null){
+ crLocal = Languages.FixLanguageTag((pbData.Meta.AudioLocale ?? Locale.DefaulT).GetEnumMemberValue());
+ }
+
+ MPDParsed streamPlaylists = MPDParser.Parse(streamPlaylistsReqResponse.ResponseContent, Languages.FindLang(crLocal), matchedUrl);
+
+ List streamServers = new List(streamPlaylists.Data.Keys);
+ options.X = options.X > streamServers.Count ? 1 : options.X;
+
+ if (streamServers.Count == 0){
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ if (options.X == 0){
+ options.X = 1;
+ }
+
+ string selectedServer = streamServers[options.X - 1];
+ ServerData selectedList = streamPlaylists.Data[selectedServer];
+
+ var videos = selectedList.video.Select(item => new VideoItem{
+ segments = item.segments,
+ pssh = item.pssh,
+ quality = item.quality,
+ bandwidth = item.bandwidth,
+ resolutionText = $"{item.quality.width}x{item.quality.height} ({Math.Round(item.bandwidth / 1024.0)}KiB/s)"
+ }).ToList();
+
+ var audios = selectedList.audio.Select(item => new AudioItem{
+ @default = item.@default,
+ segments = item.segments,
+ pssh = item.pssh,
+ language = item.language,
+ bandwidth = item.bandwidth,
+ resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s"
+ }).ToList();
+
+ videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width));
+ audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
+
+ int chosenVideoQuality;
+ if (options.QualityVideo == "best"){
+ chosenVideoQuality = videos.Count;
+ } else if (options.QualityVideo == "worst"){
+ chosenVideoQuality = 1;
+ } else{
+ var tempIndex = videos.FindIndex(a => a.quality.height + "" == options.QualityAudio);
+ if (tempIndex < 0){
+ chosenVideoQuality = videos.Count;
+ } else{
+ tempIndex++;
+ chosenVideoQuality = tempIndex;
+ }
+ }
+
+ if (chosenVideoQuality > videos.Count){
+ Console.WriteLine($"The requested quality of {chosenVideoQuality} is greater than the maximum {videos.Count}.\n[WARN] Therefore, the maximum will be capped at {videos.Count}.");
+ chosenVideoQuality = videos.Count;
+ }
+
+ chosenVideoQuality--;
+
+ int chosenAudioQuality;
+ if (options.QualityAudio == "best"){
+ chosenAudioQuality = audios.Count;
+ } else if (options.QualityAudio == "worst"){
+ chosenAudioQuality = 1;
+ } else{
+ var tempIndex = audios.FindIndex(a => a.resolutionText == options.QualityAudio);
+ if (tempIndex < 0){
+ chosenAudioQuality = audios.Count;
+ } else{
+ tempIndex++;
+ chosenAudioQuality = tempIndex;
+ }
+ }
+
+
+ if (chosenAudioQuality > audios.Count){
+ chosenAudioQuality = audios.Count;
+ }
+
+ chosenAudioQuality--;
+
+ VideoItem chosenVideoSegments = videos[chosenVideoQuality];
+ AudioItem chosenAudioSegments = audios[chosenAudioQuality];
+
+ Console.WriteLine("Servers available:");
+ foreach (var server in streamServers){
+ Console.WriteLine($"\t{server}");
+ }
+
+ Console.WriteLine("Available Video Qualities:");
+ for (int i = 0; i < videos.Count; i++){
+ Console.WriteLine($"\t[{i + 1}] {videos[i].resolutionText}");
+ }
+
+ Console.WriteLine("Available Audio Qualities:");
+ for (int i = 0; i < audios.Count; i++){
+ Console.WriteLine($"\t[{i + 1}] {audios[i].resolutionText}");
+ }
+
+ variables.Add(new Variable("height", chosenVideoSegments.quality.height, false));
+ variables.Add(new Variable("width", chosenVideoSegments.quality.width, false));
+
+ LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.Code == curStream.AudioLang);
+ if (lang == null){
+ Console.Error.WriteLine($"Unable to find language for code {curStream.AudioLang}");
+ MainWindow.ShowError($"Unable to find language for code {curStream.AudioLang}");
+ return new DownloadResponse{
+ Data = new List(),
+ Error = true,
+ FileName = "./unknown"
+ };
+ }
+
+ Console.WriteLine($"Selected quality: \n\tVideo: {chosenVideoSegments.resolutionText}\n\tAudio: {chosenAudioSegments.resolutionText}\n\tServer: {selectedServer}");
+ Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
+
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.Name ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
+
+ string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
+ .ToArray());
+ string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(CfgManager.PathVIDEOS_DIR, tempFile);
+
+ bool audioDownloaded = false, videoDownloaded = false;
+
+ if (options.DlVideoOnce && dlVideoOnce){
+ Console.WriteLine("Already downloaded video, skipping video download...");
+ return new DownloadResponse{
+ Data = files,
+ Error = dlFailed,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown"
+ };
+ }
+
+ if (options.Novids){
+ Console.WriteLine("Skipping video download...");
+ } else{
+ var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tsFile, tempTsFile, data);
+
+ tsFile = videoDownloadResult.tsFile;
+
+ if (!videoDownloadResult.Ok){
+ Console.Error.WriteLine($"DL Stats: {JsonConvert.SerializeObject(videoDownloadResult.Parts)}");
+ dlFailed = true;
+ }
+
+ dlVideoOnce = true;
+ videoDownloaded = true;
+ }
+
+
+ if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
+ var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tsFile, tempTsFile, data);
+
+ tsFile = audioDownloadResult.tsFile;
+
+ if (!audioDownloadResult.Ok){
+ Console.Error.WriteLine($"DL Stats: {JsonConvert.SerializeObject(audioDownloadResult.Parts)}");
+ dlFailed = true;
+ }
+
+ audioDownloaded = true;
+ } else if (options.Noaudio){
+ Console.WriteLine("Skipping audio download...");
+ }
+
+ if (dlFailed){
+ return new DownloadResponse{
+ Data = files,
+ Error = dlFailed,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown"
+ };
+ }
+
+ if ((chosenVideoSegments.pssh != null || chosenAudioSegments.pssh != null) && (videoDownloaded || audioDownloaded)){
+ data.DownloadProgress = new DownloadProgress(){
+ IsDownloading = true,
+ Percent = 100,
+ Time = 0,
+ DownloadSpeed = 0,
+ Doing = "Decrypting"
+ };
+
+ 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");
+
+ 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("https://pl.crunchyroll.com/drm/v1/auth", HttpMethod.Post, false, false, null);
+ decRequest.Content = reqBody;
+
+ var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest);
+
+ if (!decRequestResponse.IsOk){
+ Console.WriteLine("Request to DRM Authentication failed: ");
+ return new DownloadResponse{
+ Data = files,
+ Error = dlFailed,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown"
+ };
+ }
+
+ DrmAuthData authData = Helpers.Deserialize(decRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new DrmAuthData();
+
+
+ Dictionary authDataDict = new Dictionary
+ { { "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);
+
+ if (encryptionKeys.Count == 0){
+ Console.WriteLine("Failed to get encryption keys");
+ return new DownloadResponse{
+ Data = files,
+ Error = dlFailed,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown"
+ };
+ }
+
+ if (Path.Exists(CfgManager.PathMP4Decrypt)){
+ var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
+ var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
+ var commandBase = $"--show-progress --key {keyId}:{key}";
+ var commandVideo = commandBase + $" \"{tempTsFile}.video.enc.m4s\" \"{tempTsFile}.video.m4s\"";
+ var commandAudio = commandBase + $" \"{tempTsFile}.audio.enc.m4s\" \"{tempTsFile}.audio.m4s\"";
+
+ if (videoDownloaded){
+ Console.WriteLine("Started decrypting video");
+ var decryptVideo = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo);
+
+ if (!decryptVideo.IsOk){
+ Console.Error.WriteLine($"Decryption failed with exit code {decryptVideo.ErrorCode}");
+ try{
+ File.Move($"{tempTsFile}.video.enc.m4s", $"{tsFile}.video.enc.m4s");
+ } catch (IOException ex){
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+ } else{
+ Console.WriteLine("Decryption done for video");
+ if (!options.Nocleanup){
+ try{
+ if (File.Exists($"{tempTsFile}.video.enc.m4s")){
+ File.Delete($"{tempTsFile}.video.enc.m4s");
+ }
+
+ if (File.Exists($"{tempTsFile}.video.enc.m4s.resume")){
+ File.Delete($"{tempTsFile}.video.enc.m4s.resume");
+ }
+ } catch (Exception ex){
+ Console.WriteLine($"Failed to delete file {tempTsFile}.video.enc.m4s. Error: {ex.Message}");
+ // Handle exceptions if you need to log them or throw
+ }
+
+ try{
+ File.Move($"{tempTsFile}.video.m4s", $"{tsFile}.video.m4s");
+ } catch (IOException ex){
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+
+ files.Add(new DownloadedMedia{
+ Type = DownloadMediaType.Video,
+ Path = $"{tsFile}.video.m4s",
+ Lang = lang.Value,
+ IsPrimary = isPrimary
+ });
+ }
+ }
+ }
+
+ if (audioDownloaded){
+ Console.WriteLine("Started decrypting audio");
+ var decryptAudio = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio);
+
+ if (!decryptAudio.IsOk){
+ Console.Error.WriteLine($"Decryption failed with exit code {decryptAudio.ErrorCode}");
+ try{
+ File.Move($"{tempTsFile}.audio.enc.m4s", $"{tsFile}.audio.enc.m4s");
+ } catch (IOException ex){
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+ } else{
+ Console.WriteLine("Decryption done for audio");
+ if (!options.Nocleanup){
+ try{
+ if (File.Exists($"{tempTsFile}.audio.enc.m4s")){
+ File.Delete($"{tempTsFile}.audio.enc.m4s");
+ }
+
+ if (File.Exists($"{tempTsFile}.audio.enc.m4s.resume")){
+ File.Delete($"{tempTsFile}.audio.enc.m4s.resume");
+ }
+ } catch (Exception ex){
+ Console.WriteLine($"Failed to delete file {tempTsFile}.audio.enc.m4s. Error: {ex.Message}");
+ // Handle exceptions if you need to log them or throw
+ }
+
+ try{
+ File.Move($"{tempTsFile}.audio.m4s", $"{tsFile}.audio.m4s");
+ } catch (IOException ex){
+ Console.WriteLine($"An error occurred: {ex.Message}");
+ }
+
+ files.Add(new DownloadedMedia{
+ Type = DownloadMediaType.Audio,
+ Path = $"{tsFile}.audio.m4s",
+ Lang = lang.Value,
+ IsPrimary = isPrimary
+ });
+ }
+ }
+ }
+ } else{
+ Console.WriteLine("mp4decrypt not found, files need decryption. Decryption Keys: ");
+ MainWindow.ShowError($"mp4decrypt not found, files need decryption");
+ }
+ } else{
+ if (videoDownloaded){
+ files.Add(new DownloadedMedia{
+ Type = DownloadMediaType.Video,
+ Path = $"{tsFile}.video.m4s",
+ Lang = lang.Value,
+ IsPrimary = isPrimary
+ });
+ }
+
+ if (audioDownloaded){
+ files.Add(new DownloadedMedia{
+ Type = DownloadMediaType.Audio,
+ Path = $"{tsFile}.audio.m4s",
+ Lang = lang.Value,
+ IsPrimary = isPrimary
+ });
+ }
+ }
+ } else if (!options.Novids){
+ //TODO
+ } else if (options.Novids){
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ Console.WriteLine("Downloading skipped!");
+ }
+ }
+ } else if (options.Novids && options.Noaudio){
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ }
+
+ if (compiledChapters.Count > 0){
+ try{
+ // Parsing and constructing the file names
+ fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
+ string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.Name), variables, options.Numbers, options.Override).ToArray());
+ if (Path.IsPathRooted(outFile)){
+ tsFile = outFile;
+ } else{
+ tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile);
+ }
+
+ // Check if the path is absolute
+ bool isAbsolute = Path.IsPathRooted(outFile);
+
+ // Get all directory parts of the path except the last segment (assuming it's a file)
+ string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty();
+
+ // Initialize the cumulative path based on whether the original path is absolute or not
+ string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
+ for (int i = 0; i < directories.Length; i++){
+ // Build the path incrementally
+ cumulativePath = Path.Combine(cumulativePath, directories[i]);
+
+ // Check if the directory exists and create it if it does not
+ if (!Directory.Exists(cumulativePath)){
+ Directory.CreateDirectory(cumulativePath);
+ Console.WriteLine($"Created directory: {cumulativePath}");
+ }
+ }
+
+ // Finding language by code
+ var lang = Languages.languages.FirstOrDefault(l => l.Code == curStream?.AudioLang);
+ if (lang.Code == "und"){
+ Console.Error.WriteLine($"Unable to find language for code {curStream?.AudioLang}");
+ }
+
+ File.WriteAllText($"{tsFile}.txt", string.Join("\r\n", compiledChapters));
+
+ files.Add(new DownloadedMedia{ Path = $"{tsFile}.txt", Lang = lang, Type = DownloadMediaType.Chapters });
+ } catch{
+ Console.Error.WriteLine("Failed to write chapter file");
+ }
+ }
+
+ if (options.DlSubs.IndexOf("all") > -1){
+ options.DlSubs = new List{ "all" };
+ }
+
+ if (options.Hslang != "none"){
+ Console.WriteLine("Subtitles downloading disabled for hardsubed streams.");
+ options.SkipSubs = true;
+ }
+
+ if (options.NoSubs){
+ Console.WriteLine("Subtitles downloading disabled from nosubs flag.");
+ options.SkipSubs = true;
+ }
+
+ if (!options.SkipSubs && options.DlSubs.IndexOf("none") == -1){
+ await DownloadSubtitles(options, pbData, audDub, fileName, files);
+ } else{
+ Console.WriteLine("Subtitles downloading skipped!");
+ }
+ }
+
+ await Task.Delay(options.Waittime);
+ }
+
+
+ // variables.Add(new Variable("height", quality == 0 ? plQuality.Last().RESOLUTION.Height : plQuality[quality - 1].RESOLUTION.Height, false));
+ // variables.Add(new Variable("width", quality == 0 ? plQuality.Last().RESOLUTION.Width : plQuality[quality - 1].RESOLUTION.Width, false));
+
+ return new DownloadResponse{
+ Data = files,
+ Error = dlFailed,
+ FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(CfgManager.PathVIDEOS_DIR, fileName)) : "./unknown"
+ };
+ }
+
+ private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List files){
+ if (pbData.Meta != null && pbData.Meta.Subtitles != null && pbData.Meta.Subtitles.Count > 0){
+ List subsData = pbData.Meta.Subtitles.Values.ToList();
+ List capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List();
+ var subsDataMapped = subsData.Select(s => {
+ var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
+ return new{
+ format = s.Format,
+ url = s.Url,
+ locale = subLang,
+ language = subLang.Locale,
+ isCC = false
+ };
+ }).ToList();
+
+ var capsDataMapped = capsData.Select(s => {
+ var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
+ return new{
+ format = s.Format,
+ url = s.Url,
+ locale = subLang,
+ language = subLang.Locale,
+ isCC = true
+ };
+ }).ToList();
+
+ subsDataMapped.AddRange(capsDataMapped);
+
+ var subsArr = Languages.SortSubtitles(subsDataMapped, "language");
+
+ foreach (var subsItem in subsArr){
+ var index = subsArr.IndexOf(subsItem);
+ var langItem = subsItem.locale;
+ var sxData = new SxItem();
+ sxData.Language = langItem;
+ var isSigns = langItem.Code == audDub && !subsItem.isCC;
+ var isCc = subsItem.isCC;
+ sxData.File = Languages.SubsFile(fileName, index + "", langItem, isCc, options.CcTag, isSigns, subsItem.format);
+ sxData.Path = Path.Combine(CfgManager.PathVIDEOS_DIR, sxData.File);
+
+ // Check if the path is absolute
+ bool isAbsolute = Path.IsPathRooted(sxData.Path);
+
+ // Get all directory parts of the path except the last segment (assuming it's a file)
+ string[] directories = Path.GetDirectoryName(sxData.Path)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty();
+
+ // Initialize the cumulative path based on whether the original path is absolute or not
+ string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
+ for (int i = 0; i < directories.Length; i++){
+ // Build the path incrementally
+ cumulativePath = Path.Combine(cumulativePath, directories[i]);
+
+ // Check if the directory exists and create it if it does not
+ if (!Directory.Exists(cumulativePath)){
+ Directory.CreateDirectory(cumulativePath);
+ Console.WriteLine($"Created directory: {cumulativePath}");
+ }
+ }
+
+ // Check if any file matches the specified conditions
+ if (files.Any(a => a.Type == DownloadMediaType.Subtitle &&
+ (a.Language.CrLocale == langItem.CrLocale || a.Language.Locale == langItem.Locale) &&
+ a.Cc == isCc &&
+ a.Signs == isSigns)){
+ continue;
+ }
+
+ if (options.DlSubs.Contains("all") || options.DlSubs.Contains(langItem.CrLocale)){
+ var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url ?? string.Empty, HttpMethod.Get, false, false, null);
+
+ var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq);
+
+ if (subsAssReqResponse.IsOk){
+ if (subsItem.format == "ass"){
+ subsAssReqResponse.ResponseContent = '\ufeff' + subsAssReqResponse.ResponseContent;
+ var sBodySplit = subsAssReqResponse.ResponseContent.Split(new[]{ "\r\n" }, StringSplitOptions.None).ToList();
+ // Insert 'ScaledBorderAndShadow: yes' after the second line
+ if (sBodySplit.Count > 2)
+ sBodySplit.Insert(2, "ScaledBorderAndShadow: yes");
+
+ // Rejoin the lines back into a single string
+ subsAssReqResponse.ResponseContent = string.Join("\r\n", sBodySplit);
+
+ // Extract the title from the second line and remove 'Title: ' prefix
+ if (sBodySplit.Count > 1){
+ sxData.Title = sBodySplit[1].Replace("Title: ", "");
+ sxData.Title = $"{langItem.Language} / {sxData.Title}";
+ var keysList = FontsManager.ExtractFontsFromAss(subsAssReqResponse.ResponseContent);
+ sxData.Fonts = FontsManager.Instance.GetDictFromKeyList(keysList);
+ }
+ }
+
+ File.WriteAllText(sxData.Path, subsAssReqResponse.ResponseContent);
+ Console.WriteLine($"Subtitle downloaded: ${sxData.File}");
+ files.Add(new DownloadedMedia{
+ Type = DownloadMediaType.Subtitle,
+ Cc = isCc,
+ Signs = isSigns,
+ Path = sxData.Path,
+ File = sxData.File,
+ Title = sxData.Title,
+ Fonts = sxData.Fonts,
+ Language = sxData.Language,
+ Lang = sxData.Language
+ });
+ } else{
+ Console.WriteLine($"Failed to download subtitle: ${sxData.File}");
+ }
+ }
+ }
+ } else{
+ Console.WriteLine("Can\'t find urls for subtitles!");
+ }
+ }
+
+ private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadVideo(VideoItem chosenVideoSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data){
+ // Prepare for video download
+ int totalParts = chosenVideoSegments.segments.Count;
+ int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
+ string mathMsg = $"({mathParts}*{options.Partsize})";
+ Console.WriteLine($"Total parts in video stream: {totalParts} {mathMsg}");
+
+ if (Path.IsPathRooted(outFile)){
+ tsFile = outFile;
+ } else{
+ tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile);
+ }
+
+ // var split = outFile.Split(Path.DirectorySeparatorChar).AsSpan().Slice(0, -1).ToArray();
+ // Check if the path is absolute
+ bool isAbsolute = Path.IsPathRooted(outFile);
+
+ // Get all directory parts of the path except the last segment (assuming it's a file)
+ string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty();
+
+ // Initialize the cumulative path based on whether the original path is absolute or not
+ string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
+ for (int i = 0; i < directories.Length; i++){
+ // Build the path incrementally
+ cumulativePath = Path.Combine(cumulativePath, directories[i]);
+
+ // Check if the directory exists and create it if it does not
+ if (!Directory.Exists(cumulativePath)){
+ Directory.CreateDirectory(cumulativePath);
+ Console.WriteLine($"Created directory: {cumulativePath}");
+ }
+ }
+
+ M3U8Json videoJson = new M3U8Json{
+ Segments = chosenVideoSegments.segments.Cast().ToList()
+ };
+
+ var videoDownloader = new HlsDownloader(new HlsOptions{
+ Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s",
+ Timeout = options.Timeout,
+ M3U8Json = videoJson,
+ // BaseUrl = chunkPlaylist.BaseUrl,
+ Threads = options.Partsize,
+ FsRetryTime = options.FsRetryTime * 1000,
+ Override = options.Force,
+ }, data, true, false);
+
+ var videoDownloadResult = await videoDownloader.Download();
+
+ return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile);
+ }
+
+ private async Task<(bool Ok, PartsData Parts, string tsFile)> DownloadAudio(AudioItem chosenAudioSegments, CrDownloadOptions options, string outFile, string tsFile, string tempTsFile, CrunchyEpMeta data){
+ // Prepare for audio download
+ int totalParts = chosenAudioSegments.segments.Count;
+ int mathParts = (int)Math.Ceiling((double)totalParts / options.Partsize);
+ string mathMsg = $"({mathParts}*{options.Partsize})";
+ Console.WriteLine($"Total parts in audio stream: {totalParts} {mathMsg}");
+
+ if (Path.IsPathRooted(outFile)){
+ tsFile = outFile;
+ } else{
+ tsFile = Path.Combine(CfgManager.PathVIDEOS_DIR, outFile);
+ }
+
+ // Check if the path is absolute
+ bool isAbsolute = Path.IsPathRooted(outFile);
+
+ // Get all directory parts of the path except the last segment (assuming it's a file)
+ string[] directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? Array.Empty();
+
+ // Initialize the cumulative path based on whether the original path is absolute or not
+ string cumulativePath = isAbsolute ? "" : CfgManager.PathVIDEOS_DIR;
+ for (int i = 0; i < directories.Length; i++){
+ // Build the path incrementally
+ cumulativePath = Path.Combine(cumulativePath, directories[i]);
+
+ // Check if the directory exists and create it if it does not
+ if (!Directory.Exists(cumulativePath)){
+ Directory.CreateDirectory(cumulativePath);
+ Console.WriteLine($"Created directory: {cumulativePath}");
+ }
+ }
+
+ M3U8Json audioJson = new M3U8Json{
+ Segments = chosenAudioSegments.segments.Cast().ToList()
+ };
+
+ var audioDownloader = new HlsDownloader(new HlsOptions{
+ Output = chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s",
+ Timeout = options.Timeout,
+ M3U8Json = audioJson,
+ // BaseUrl = chunkPlaylist.BaseUrl,
+ Threads = options.Partsize,
+ FsRetryTime = options.FsRetryTime * 1000,
+ Override = options.Force,
+ }, data, false, true);
+
+ var audioDownloadResult = await audioDownloader.Download();
+
+
+ return (audioDownloadResult.Ok, audioDownloadResult.Parts, tsFile);
+ }
+
+ private async Task FetchNoDrmPlaybackData(string currentMediaId, PlaybackData pbData){
+ var playbackRequestNonDrm = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/{currentMediaId}/console/switch/play", HttpMethod.Get, true, true, null);
+
+ var playbackRequestNonDrmResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequestNonDrm);
+
+ if (playbackRequestNonDrmResponse.IsOk && playbackRequestNonDrmResponse.ResponseContent != string.Empty){
+ CrunchyNoDrmStream? playStream = JsonConvert.DeserializeObject(playbackRequestNonDrmResponse.ResponseContent, SettingsJsonSerializerSettings);
+ CrunchyStreams derivedPlayCrunchyStreams = new CrunchyStreams();
+ if (playStream != null){
+ if (playStream.HardSubs != null)
+ foreach (var hardsub in playStream.HardSubs){
+ var stream = hardsub.Value;
+ derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
+ Url = stream.Url,
+ HardsubLocale = Helpers.ConvertStringToLocale(stream.Hlang)
+ };
+ }
+
+ derivedPlayCrunchyStreams[""] = new StreamDetails{
+ Url = playStream.Url,
+ HardsubLocale = Locale.DefaulT
+ };
+
+ if (pbData.Data != null) pbData.Data[0]["adaptive_switch_dash"] = derivedPlayCrunchyStreams;
+ }
+ } else{
+ Console.WriteLine("Non-DRM Request Stream URLs FAILED!");
+ }
+ }
+
+ private async Task<(bool IsOk, PlaybackData pbData)> FetchPlaybackData(string mediaId, CrunchyEpMetaData epMeta){
+ PlaybackData temppbData = new PlaybackData{ Total = 0, Data = new List>>() };
+ bool ok = true;
+
+ HttpRequestMessage playbackRequest;
+ (bool IsOk, string ResponseContent) playbackRequestResponse;
+
+ if (_api == "android"){
+ NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
+
+ query["force_locale"] = "";
+ query["preferred_audio_language"] = "ja-JP";
+ query["Policy"] = CmsToken?.Cms.Policy;
+ query["Signature"] = CmsToken?.Cms.Signature;
+ query["Key-Pair-Id"] = CmsToken?.Cms.KeyPairId;
+
+
+ playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.BetaCms}{CmsToken?.Cms.Bucket}/videos/{mediaId}/streams?", HttpMethod.Get, true, true, query);
+
+ playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
+
+ if (playbackRequestResponse.IsOk){
+ var androidTempData = Helpers.Deserialize(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
+ temppbData = new PlaybackData(){
+ Data = androidTempData.streams, Total = androidTempData.streams.Count,
+ Meta = new PlaybackMeta(){
+ MediaId = androidTempData.media_id, Subtitles = androidTempData.subtitles, Bifs = androidTempData.bifs, Versions = androidTempData.versions, AudioLocale = androidTempData.audio_locale,
+ ClosedCaptions = androidTempData.closed_captions, Captions = androidTempData.captions
+ }
+ };
+ } else{
+ NameValueCollection query2 = HttpUtility.ParseQueryString(new UriBuilder().Query);
+
+ query2["preferred_audio_language"] = "ja-JP";
+ query2["Policy"] = CmsToken?.Cms.Policy;
+ query2["Signature"] = CmsToken?.Cms.Signature;
+ query2["Key-Pair-Id"] = CmsToken?.Cms.KeyPairId;
+
+ playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.ApiBeta}{epMeta.Playback}?", HttpMethod.Get, true, true, query2);
+
+ playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
+
+ if (playbackRequestResponse.IsOk){
+ temppbData = Helpers.Deserialize(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
+ new PlaybackData{ Total = 0, Data = new List>>() };
+ } else{
+ Console.WriteLine("'Fallback Request Stream URLs FAILED!'");
+ ok = playbackRequestResponse.IsOk;
+ }
+ }
+ } else{
+ playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/videos/{mediaId}/streams", HttpMethod.Get, true, false, null);
+
+ playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
+
+ if (playbackRequestResponse.IsOk){
+ temppbData = Helpers.Deserialize(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
+ new PlaybackData{ Total = 0, Data = new List>>() };
+ } else{
+ Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
+
+ playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.ApiBeta}{epMeta.Playback}", HttpMethod.Get, true, true, null);
+
+ playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
+
+ if (playbackRequestResponse.IsOk){
+ temppbData = Helpers.Deserialize(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
+ new PlaybackData{ Total = 0, Data = new List>>() };
+ } else{
+ Console.WriteLine("'Fallback Request Stream URLs FAILED!'");
+ ok = playbackRequestResponse.IsOk;
+ }
+ }
+ }
+
+ return (IsOk: ok, pbData: temppbData);
+ }
+
+ private async Task ParseChapters(string currentMediaId, List compiledChapters){
+ var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null);
+
+ var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest);
+
+ if (showRequestResponse.IsOk){
+ JObject jObject = JObject.Parse(showRequestResponse.ResponseContent);
+
+ CrunchyChapters chapterData = new CrunchyChapters();
+ chapterData.lastUpdate = jObject["lastUpdate"]?.ToObject();
+ chapterData.mediaId = jObject["mediaId"]?.ToObject();
+ chapterData.Chapters = new List();
+
+ foreach (var property in jObject.Properties()){
+ // Check if the property value is an object and the property is not one of the known non-dictionary properties
+ if (property.Value.Type == JTokenType.Object && property.Name != "lastUpdate" && property.Name != "mediaId"){
+ // Deserialize the property value into a CrunchyChapter and add it to the dictionary
+ CrunchyChapter chapter = property.Value.ToObject();
+ chapterData.Chapters.Add(chapter);
+ }
+ }
+
+ if (chapterData.Chapters.Count > 0){
+ chapterData.Chapters.Sort((a, b) => {
+ if (a.start != null && b.start != null)
+ return a.start.Value - b.start.Value;
+ return 0;
+ });
+
+ if (!((chapterData.Chapters.Any(c => c.type == "intro")) || chapterData.Chapters.Any(c => c.type == "recap"))){
+ int chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}=00:00:00.00");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode");
+ }
+
+ foreach (CrunchyChapter chapter in chapterData.Chapters){
+ if (chapter.start == null || chapter.end == null) continue;
+
+ DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ DateTime startTime = epoch.AddSeconds(chapter.start.Value);
+ DateTime endTime = epoch.AddSeconds(chapter.end.Value);
+
+ string startFormatted = startTime.ToString("HH:mm:ss") + ".00";
+ string endFormatted = endTime.ToString("HH:mm:ss") + ".00";
+
+ int chapterNumber = (compiledChapters.Count / 2) + 1;
+ if (chapter.type == "intro"){
+ if (chapter.start > 0){
+ compiledChapters.Add($"CHAPTER{chapterNumber}=00:00:00.00");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Prologue");
+ }
+
+ chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}={startFormatted}");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Opening");
+ chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode");
+ } else{
+ string formattedChapterType = char.ToUpper(chapter.type[0]) + chapter.type.Substring(1);
+ chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}={startFormatted}");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME={formattedChapterType} Start");
+ chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME={formattedChapterType} End");
+ }
+ }
+ }
+ } else{
+ Console.WriteLine("Chapter request failed, attempting old API ");
+
+ showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/datalab-intro-v2/{currentMediaId}.json", HttpMethod.Get, true, true, null);
+
+ showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest);
+
+ if (showRequestResponse.IsOk){
+ CrunchyOldChapter chapterData = JsonConvert.DeserializeObject(showRequestResponse.ResponseContent);
+
+ DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ DateTime startTime = epoch.AddSeconds(chapterData.startTime);
+ DateTime endTime = epoch.AddSeconds(chapterData.endTime);
+
+ string[] startTimeParts = startTime.ToString(CultureInfo.CurrentCulture).Split('.');
+ string[] endTimeParts = endTime.ToString(CultureInfo.CurrentCulture).Split('.');
+
+ string startMs = startTimeParts.Length > 1 ? startTimeParts[1] : "00";
+ string endMs = endTimeParts.Length > 1 ? endTimeParts[1] : "00";
+
+ string startFormatted = startTime.ToString("HH:mm:ss") + "." + startMs;
+ string endFormatted = endTime.ToString("HH:mm:ss") + "." + endMs;
+
+ int chapterNumber = (compiledChapters.Count / 2) + 1;
+ if (chapterData.startTime > 1){
+ compiledChapters.Add($"CHAPTER{chapterNumber}=00:00:00.00");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Prologue");
+ }
+
+ chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}={startFormatted}");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Opening");
+ chapterNumber = (compiledChapters.Count / 2) + 1;
+ compiledChapters.Add($"CHAPTER{chapterNumber}={endFormatted}");
+ compiledChapters.Add($"CHAPTER{chapterNumber}NAME=Episode");
+ } else{
+ Console.WriteLine("Old Chapter API request failed");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Downloader/History.cs b/Downloader/History.cs
new file mode 100644
index 0000000..f679769
--- /dev/null
+++ b/Downloader/History.cs
@@ -0,0 +1,411 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Avalonia.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CRD.Utils;
+using CRD.Utils.Structs;
+using CRD.Views;
+using Newtonsoft.Json;
+using ReactiveUI;
+
+namespace CRD.Downloader;
+
+public class History(Crunchyroll crunInstance){
+ public async Task UpdateSeries(string seriesId, string? seasonId){
+ await crunInstance.CrAuth.RefreshToken(true);
+
+ CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja");
+
+ if (parsedSeries == null){
+ Console.WriteLine("Parse Data Invalid");
+ return;
+ }
+
+ var result = crunInstance.CrSeries.ParseSeriesResult(parsedSeries);
+ Dictionary episodes = new Dictionary();
+
+ foreach (int season in result.Keys){
+ foreach (var key in result[season].Keys){
+ var s = result[season][key];
+ if (seasonId != null && s.Id != seasonId) continue;
+ var seasonData = await crunInstance.CrSeries.GetSeasonDataById(s);
+ UpdateWithSeasonData(seasonData);
+ }
+ }
+ }
+
+ private void UpdateHistoryFile(){
+ CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, crunInstance.HistoryList);
+ }
+
+ public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){
+ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
+
+ if (historySeries != null){
+ var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId);
+
+ if (historySeason != null){
+ var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
+
+ if (historyEpisode != null){
+ historyEpisode.WasDownloaded = true;
+ historySeason.UpdateDownloaded();
+ return;
+ }
+ }
+ }
+
+ MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
+ }
+
+
+ public async void UpdateWithEpisode(CrunchyEpisode episodeParam){
+ var episode = episodeParam;
+
+ if (episode.Versions != null){
+ var version = episode.Versions.Find(a => a.Original);
+ if (version.AudioLocale != episode.AudioLocale){
+ var episodeById = await crunInstance.CrEpisode.ParseEpisodeById(version.Guid, "");
+ if (episodeById?.Data != null){
+ if (episodeById.Value.Total != 1){
+ MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 1));
+ return;
+ }
+
+ episode = episodeById.Value.Data.First();
+ }
+ }
+ }
+
+
+ var seriesId = episode.SeriesId;
+ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
+ if (historySeries != null){
+ var historySeason = historySeries.Seasons.Find(s => s.SeasonId == episode.SeasonId);
+
+ if (historySeason != null){
+ if (historySeason.EpisodesList.All(e => e.EpisodeId != episode.Id)){
+ var newHistoryEpisode = new HistoryEpisode{
+ EpisodeTitle = episode.Title,
+ EpisodeId = episode.Id,
+ Episode = episode.Episode,
+ };
+
+ historySeason.EpisodesList.Add(newHistoryEpisode);
+
+ historySeason.EpisodesList.Sort(new NumericStringPropertyComparer());
+ }
+ } else{
+ var newSeason = NewHistorySeason(episode);
+
+ historySeries.Seasons.Add(newSeason);
+
+ historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum).ToList();
+ }
+ historySeries.UpdateNewEpisodes();
+ } else{
+ var newHistorySeries = new HistorySeries{
+ SeriesTitle = episode.SeriesTitle,
+ SeriesId = episode.SeriesId,
+ Seasons =[],
+ };
+ crunInstance.HistoryList.Add(newHistorySeries);
+ var newSeason = NewHistorySeason(episode);
+
+ var series = await crunInstance.CrSeries.SeriesById(seriesId);
+ if (series?.Data != null){
+ newHistorySeries.SeriesDescription = series.Data.First().Description;
+ newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
+ }
+
+ newHistorySeries.Seasons.Add(newSeason);
+ newHistorySeries.UpdateNewEpisodes();
+ }
+
+ var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList();
+ crunInstance.HistoryList.Clear();
+ foreach (var item in sortedList){
+ crunInstance.HistoryList.Add(item);
+ }
+
+ UpdateHistoryFile();
+ }
+
+ public async void UpdateWithSeasonData(CrunchyEpisodeList seasonData){
+ if (seasonData.Data != null){
+ var firstEpisode = seasonData.Data.First();
+ var seriesId = firstEpisode.SeriesId;
+ var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
+ if (historySeries != null){
+ var historySeason = historySeries.Seasons.Find(s => s.SeasonId == firstEpisode.SeasonId);
+
+ if (historySeason != null){
+ foreach (var crunchyEpisode in seasonData.Data){
+ if (historySeason.EpisodesList.All(e => e.EpisodeId != crunchyEpisode.Id)){
+ var newHistoryEpisode = new HistoryEpisode{
+ EpisodeTitle = crunchyEpisode.Title,
+ EpisodeId = crunchyEpisode.Id,
+ Episode = crunchyEpisode.Episode,
+ };
+
+ historySeason.EpisodesList.Add(newHistoryEpisode);
+ }
+ }
+
+ historySeason.EpisodesList.Sort(new NumericStringPropertyComparer());
+ } else{
+ var newSeason = NewHistorySeason(seasonData, firstEpisode);
+
+ newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
+
+ historySeries.Seasons.Add(newSeason);
+
+ historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum).ToList();
+ }
+ historySeries.UpdateNewEpisodes();
+ } else{
+ var newHistorySeries = new HistorySeries{
+ SeriesTitle = firstEpisode.SeriesTitle,
+ SeriesId = firstEpisode.SeriesId,
+ Seasons =[],
+ };
+ crunInstance.HistoryList.Add(newHistorySeries);
+
+ var newSeason = NewHistorySeason(seasonData, firstEpisode);
+
+ newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
+
+ var series = await crunInstance.CrSeries.SeriesById(seriesId);
+ if (series?.Data != null){
+ newHistorySeries.SeriesDescription = series.Data.First().Description;
+ newHistorySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
+ }
+
+
+ newHistorySeries.Seasons.Add(newSeason);
+ newHistorySeries.UpdateNewEpisodes();
+ }
+ }
+
+ var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList();
+ crunInstance.HistoryList.Clear();
+ foreach (var item in sortedList){
+ crunInstance.HistoryList.Add(item);
+ }
+
+ UpdateHistoryFile();
+ }
+
+ private string GetSeriesThumbnail(CrSeriesBase series){
+ // var series = await crunInstance.CrSeries.SeriesById(seriesId);
+
+ if ((series.Data ?? Array.Empty()).First().Images.PosterTall?.Count > 0){
+ return series.Data.First().Images.PosterTall.First().First(e => e.Height == 360).Source;
+ }
+
+ return "";
+ }
+
+ private static bool CheckStringForSpecial(string identifier){
+ // Regex pattern to match any sequence that does NOT contain "|S" followed by one or more digits immediately after
+ string pattern = @"^(?!.*\|S\d+).*";
+
+ // Use Regex.IsMatch to check if the identifier matches the pattern
+ return Regex.IsMatch(identifier, pattern);
+ }
+
+ private static HistorySeason NewHistorySeason(CrunchyEpisodeList seasonData, CrunchyEpisode firstEpisode){
+ var newSeason = new HistorySeason{
+ SeasonTitle = firstEpisode.SeasonTitle,
+ SeasonId = firstEpisode.SeasonId,
+ SeasonNum = firstEpisode.SeasonNumber + "",
+ EpisodesList =[],
+ SpecialSeason = CheckStringForSpecial(firstEpisode.Identifier)
+ };
+
+ foreach (var crunchyEpisode in seasonData.Data!){
+ var newHistoryEpisode = new HistoryEpisode{
+ EpisodeTitle = crunchyEpisode.Title,
+ EpisodeId = crunchyEpisode.Id,
+ Episode = crunchyEpisode.Episode,
+ };
+
+ newSeason.EpisodesList.Add(newHistoryEpisode);
+ }
+
+ return newSeason;
+ }
+
+ private static HistorySeason NewHistorySeason(CrunchyEpisode episode){
+ var newSeason = new HistorySeason{
+ SeasonTitle = episode.SeasonTitle,
+ SeasonId = episode.SeasonId,
+ SeasonNum = episode.SeasonNumber + "",
+ EpisodesList =[],
+ };
+
+ var newHistoryEpisode = new HistoryEpisode{
+ EpisodeTitle = episode.Title,
+ EpisodeId = episode.Id,
+ Episode = episode.Episode,
+ };
+
+ newSeason.EpisodesList.Add(newHistoryEpisode);
+
+
+ return newSeason;
+ }
+}
+
+public class NumericStringPropertyComparer : IComparer{
+ public int Compare(HistoryEpisode x, HistoryEpisode y){
+ if (int.TryParse(x.Episode, out int xInt) && int.TryParse(y.Episode, out int yInt)){
+ return xInt.CompareTo(yInt);
+ }
+
+ // Fall back to string comparison if not parseable as integers
+ return String.Compare(x.Episode, y.Episode, StringComparison.Ordinal);
+ }
+}
+
+public class HistorySeries : INotifyPropertyChanged{
+ [JsonProperty("series_title")]
+ public string? SeriesTitle{ get; set; }
+
+ [JsonProperty("series_id")]
+ public string? SeriesId{ get; set; }
+
+ [JsonProperty("series_description")]
+ public string? SeriesDescription{ get; set; }
+
+ [JsonProperty("series_thumbnail_url")]
+ public string? ThumbnailImageUrl{ get; set; }
+
+ [JsonProperty("series_new_episodes")]
+ public int NewEpisodes{ get; set; }
+
+ [JsonIgnore]
+ public Bitmap? ThumbnailImage{ get; set; }
+
+ [JsonProperty("series_season_list")]
+ public required List Seasons{ get; set; }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public async Task LoadImage(){
+ try{
+ using (var client = new HttpClient()){
+ var response = await client.GetAsync(ThumbnailImageUrl);
+ response.EnsureSuccessStatusCode();
+ using (var stream = await response.Content.ReadAsStreamAsync()){
+ ThumbnailImage = new Bitmap(stream);
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
+ }
+ }
+ } catch (Exception ex){
+ // Handle exceptions
+ Console.WriteLine("Failed to load image: " + ex.Message);
+ }
+ }
+
+ public void UpdateNewEpisodes(){
+ int count = 0;
+ bool foundWatched = false;
+
+ // Iterate over the Seasons list from the end to the beginning
+ for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
+
+ if (Seasons[i].SpecialSeason == true){
+ continue;
+ }
+
+ // Iterate over the Episodes from the end to the beginning
+ for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
+ if (!Seasons[i].EpisodesList[j].WasDownloaded){
+ count++;
+ } else{
+ foundWatched = true;
+ }
+ }
+ }
+ NewEpisodes = count;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
+ }
+
+ public async Task FetchData(string? seasonId){
+ await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId);
+ }
+}
+
+public class HistorySeason : INotifyPropertyChanged{
+ [JsonProperty("season_title")]
+ public string? SeasonTitle{ get; set; }
+
+ [JsonProperty("season_id")]
+ public string? SeasonId{ get; set; }
+
+ [JsonProperty("season_cr_season_number")]
+ public string? SeasonNum{ get; set; }
+
+ [JsonProperty("season_special_season")]
+ public bool? SpecialSeason{ get; set; }
+ [JsonIgnore]
+ public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
+
+ [JsonProperty("season_downloaded_episodes")]
+ public int DownloadedEpisodes{ get; set; }
+
+ [JsonProperty("season_episode_list")]
+ public required List EpisodesList{ get; set; }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public void UpdateDownloaded(string? EpisodeId){
+ if (!string.IsNullOrEmpty(EpisodeId)){
+ EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded();
+ }
+
+ DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
+ CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
+ }
+
+ public void UpdateDownloaded(){
+ DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
+ CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
+ }
+}
+
+public partial class HistoryEpisode : INotifyPropertyChanged{
+ [JsonProperty("episode_title")]
+ public string? EpisodeTitle{ get; set; }
+
+ [JsonProperty("episode_id")]
+ public string? EpisodeId{ get; set; }
+
+ [JsonProperty("episode_cr_episode_number")]
+ public string? Episode{ get; set; }
+
+ [JsonProperty("episode_was_downloaded")]
+ public bool WasDownloaded{ get; set; }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public void ToggleWasDownloaded(){
+ WasDownloaded = !WasDownloaded;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
+ }
+
+ public void DownloadEpisode(){
+ Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang);
+
+ }
+}
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..174e972
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,20 @@
+using System;
+using Avalonia;
+
+namespace CRD;
+
+sealed class Program{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
\ No newline at end of file
diff --git a/Styling/ControlsGalleryStyles.axaml b/Styling/ControlsGalleryStyles.axaml
new file mode 100644
index 0000000..50cb62c
--- /dev/null
+++ b/Styling/ControlsGalleryStyles.axaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Utils/CustomList/RefreshableObservableCollection.cs b/Utils/CustomList/RefreshableObservableCollection.cs
new file mode 100644
index 0000000..e9c4199
--- /dev/null
+++ b/Utils/CustomList/RefreshableObservableCollection.cs
@@ -0,0 +1,10 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace CRD.Utils.CustomList;
+
+public class RefreshableObservableCollection : ObservableCollection{
+ public void Refresh(){
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+}
\ No newline at end of file
diff --git a/Utils/DRM/ContentKey.cs b/Utils/DRM/ContentKey.cs
new file mode 100644
index 0000000..a36328f
--- /dev/null
+++ b/Utils/DRM/ContentKey.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using System.Text.Json.Serialization;
+
+namespace CRD.Utils.DRM;
+
+[Serializable]
+public class ContentKey{
+ [JsonPropertyName("key_id")] public byte[] KeyID{ get; set; }
+
+ [JsonPropertyName("type")] public string Type{ get; set; }
+
+ [JsonPropertyName("bytes")] public byte[] Bytes{ get; set; } // key
+
+ [NotMapped]
+ [JsonPropertyName("permissions")]
+ public List Permissions{
+ get{ return PermissionsString.Split(",").ToList(); }
+ set{ PermissionsString = string.Join(",", value); }
+ }
+
+ [JsonIgnore] public string PermissionsString{ get; set; }
+
+ public override string ToString(){
+ return $"{BitConverter.ToString(KeyID).Replace("-", "").ToLower()}:{BitConverter.ToString(Bytes).Replace("-", "").ToLower()}";
+ }
+}
\ No newline at end of file
diff --git a/Utils/DRM/CryptoUtils.cs b/Utils/DRM/CryptoUtils.cs
new file mode 100644
index 0000000..03a75d9
--- /dev/null
+++ b/Utils/DRM/CryptoUtils.cs
@@ -0,0 +1,29 @@
+namespace CRD.Utils.DRM;
+
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Macs;
+using Org.BouncyCastle.Crypto.Parameters;
+using System.Security.Cryptography;
+
+public class CryptoUtils{
+ public static byte[] GetHMACSHA256Digest(byte[] data, byte[] key){
+ return new HMACSHA256(key).ComputeHash(data);
+ }
+
+ public static byte[] GetCMACDigest(byte[] data, byte[] key){
+ IBlockCipher cipher = new AesEngine();
+ IMac mac = new CMac(cipher, 128);
+
+ KeyParameter keyParam = new KeyParameter(key);
+
+ mac.Init(keyParam);
+
+ mac.BlockUpdate(data, 0, data.Length);
+
+ byte[] outBytes = new byte[16];
+
+ mac.DoFinal(outBytes, 0);
+ return outBytes;
+ }
+}
\ No newline at end of file
diff --git a/Utils/DRM/PSSHbox.cs b/Utils/DRM/PSSHbox.cs
new file mode 100644
index 0000000..fc48c70
--- /dev/null
+++ b/Utils/DRM/PSSHbox.cs
@@ -0,0 +1,58 @@
+namespace CRD.Utils.DRM;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+class PSSHBox{
+ static readonly byte[] PSSH_HEADER = new byte[]{ 0x70, 0x73, 0x73, 0x68 };
+
+ public List KIDs{ get; set; } = new List();
+ public byte[] Data{ get; set; }
+
+ PSSHBox(List kids, byte[] data){
+ KIDs = kids;
+ Data = data;
+ }
+
+ public static PSSHBox FromByteArray(byte[] psshbox){
+ using var stream = new System.IO.MemoryStream(psshbox);
+
+ stream.Seek(4, System.IO.SeekOrigin.Current);
+ byte[] header = new byte[4];
+ stream.Read(header, 0, 4);
+
+ if (!header.SequenceEqual(PSSH_HEADER))
+ throw new Exception("Not a pssh box");
+
+ stream.Seek(20, System.IO.SeekOrigin.Current);
+ byte[] kidCountBytes = new byte[4];
+ stream.Read(kidCountBytes, 0, 4);
+
+ if (BitConverter.IsLittleEndian)
+ Array.Reverse(kidCountBytes);
+ uint kidCount = BitConverter.ToUInt32(kidCountBytes);
+
+ List kids = new List();
+ for (int i = 0; i < kidCount; i++){
+ byte[] kid = new byte[16];
+ stream.Read(kid);
+ kids.Add(kid);
+ }
+
+ byte[] dataLengthBytes = new byte[4];
+ stream.Read(dataLengthBytes);
+
+ if (BitConverter.IsLittleEndian)
+ Array.Reverse(dataLengthBytes);
+ uint dataLength = BitConverter.ToUInt32(dataLengthBytes);
+
+ if (dataLength == 0)
+ return new PSSHBox(kids, null);
+
+ byte[] data = new byte[dataLength];
+ stream.Read(data);
+
+ return new PSSHBox(kids, data);
+ }
+}
\ No newline at end of file
diff --git a/Utils/DRM/Protocol.cs b/Utils/DRM/Protocol.cs
new file mode 100644
index 0000000..97fcf53
--- /dev/null
+++ b/Utils/DRM/Protocol.cs
@@ -0,0 +1,128 @@
+// using System;
+// using System.Collections.Generic;
+// using System.IO;
+// using ProtoBuf;
+//
+// namespace CRD.Utils.DRM;
+//
+// public class ClientIdentification{
+// /** Type of factory-provisioned device root of trust. Optional. */
+// public ClientIdentification_TokenType type{ get; set; }
+//
+// /** Factory-provisioned device root of trust. Required. */
+// public byte[] token{ get; set; }
+//
+// /** Optional client information name/value pairs. */
+// public List clientInfo{ get; set; }
+//
+// /** Client token generated by the content provider. Optional. */
+// public byte[] providerClientToken{ get; set; }
+//
+// /**
+// * Number of licenses received by the client to which the token above belongs.
+// * Only present if client_token is specified.
+// */
+// public double licenseCounter{ get; set; }
+//
+// /** List of non-baseline client capabilities. */
+// public ClientIdentification_ClientCapabilities? clientCapabilities{ get; set; }
+//
+// /** Serialized VmpData message. Optional. */
+// public byte[] vmpData{ get; set; }
+//
+// /** Optional field that may contain additional provisioning credentials. */
+// public List deviceCredentials{ get; set; }
+//
+// public static ClientIdentification decode(byte[] input){
+// return Serializer.Deserialize(new MemoryStream(input));
+// }
+// }
+//
+// public struct ClientIdentification_NameValue{
+// public string name{ get; set; }
+// public string value{ get; set; }
+// }
+//
+// public enum ClientIdentification_TokenType{
+// KEYBOX = 0,
+// DRM_DEVICE_CERTIFICATE = 1,
+// REMOTE_ATTESTATION_CERTIFICATE = 2,
+// OEM_DEVICE_CERTIFICATE = 3,
+// UNRECOGNIZED = -1
+// }
+//
+// public struct ClientIdentification_ClientCredentials{
+// public ClientIdentification_TokenType type{ get; set; }
+// public byte[] token{ get; set; }
+// }
+//
+// /**
+// * Capabilities which not all clients may support. Used for the license
+// * exchange protocol only.
+// */
+// public class ClientIdentification_ClientCapabilities{
+// public bool clientToken{ get; set; }
+// public bool sessionToken{ get; set; }
+// public bool videoResolutionConstraints{ get; set; }
+// public ClientIdentification_ClientCapabilities_HdcpVersion maxHdcpVersion{ get; set; }
+// public double oemCryptoApiVersion{ get; set; }
+//
+// /**
+// * Client has hardware support for protecting the usage table, such as
+// * storing the generation number in secure memory. For Details, see:
+// * Widevine Modular DRM Security Integration Guide for CENC
+// */
+// public bool antiRollbackUsageTable{ get; set; }
+//
+// /** The client shall report |srm_version| if available. */
+// public double srmVersion{ get; set; }
+//
+// /**
+// * A device may have SRM data, and report a version, but may not be capable
+// * of updating SRM data.
+// */
+// public bool canUpdateSrm{ get; set; }
+//
+// public ClientIdentification_ClientCapabilities_CertificateKeyType[] supportedCertificateKeyType{ get; set; }
+// public ClientIdentification_ClientCapabilities_AnalogOutputCapabilities analogOutputCapabilities{ get; set; }
+// public bool canDisableAnalogOutput{ get; set; }
+//
+// /**
+// * Clients can indicate a performance level supported by OEMCrypto.
+// * This will allow applications and providers to choose an appropriate
+// * quality of content to serve. Currently defined tiers are
+// * 1 (low), 2 (medium) and 3 (high). Any other value indicates that
+// * the resource rating is unavailable or reporting erroneous values
+// * for that device. For details see,
+// * Widevine Modular DRM Security Integration Guide for CENC
+// */
+// public double resourceRatingTier{ get; set; }
+// }
+//
+// public enum ClientIdentification_ClientCapabilities_HdcpVersion{
+// HDCP_NONE = 0,
+// HDCP_V1 = 1,
+// HDCP_V2 = 2,
+// HDCP_V2_1 = 3,
+// HDCP_V2_2 = 4,
+// HDCP_V2_3 = 5,
+// HDCP_NO_DIGITAL_OUTPUT = 255,
+// UNRECOGNIZED = -1
+// }
+//
+// public enum ClientIdentification_ClientCapabilities_AnalogOutputCapabilities{
+// ANALOG_OUTPUT_UNKNOWN = 0,
+// ANALOG_OUTPUT_NONE = 1,
+// ANALOG_OUTPUT_SUPPORTED = 2,
+// ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3,
+// UNRECOGNIZED = -1
+// }
+//
+// public enum ClientIdentification_ClientCapabilities_CertificateKeyType{
+// RSA_2048 = 0,
+// RSA_3072 = 1,
+// ECC_SECP256R1 = 2,
+// ECC_SECP384R1 = 3,
+// ECC_SECP521R1 = 4,
+// UNRECOGNIZED = -1
+// }
\ No newline at end of file
diff --git a/Utils/DRM/Session.cs b/Utils/DRM/Session.cs
new file mode 100644
index 0000000..38e2ec4
--- /dev/null
+++ b/Utils/DRM/Session.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Digests;
+using Org.BouncyCastle.Crypto.Encodings;
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Signers;
+using Org.BouncyCastle.OpenSsl;
+using ProtoBuf;
+
+namespace CRD.Utils.DRM;
+
+public struct ContentDecryptionModule{
+ public byte[] privateKey{ get; set; }
+ public byte[] identifierBlob{ get; set; }
+}
+
+public class DerivedKeys{
+ public byte[] Auth1{ get; set; }
+ public byte[] Auth2{ get; set; }
+ public byte[] Enc{ get; set; }
+}
+
+public class Session{
+ public byte[] WIDEVINE_SYSTEM_ID = new byte[]{ 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 };
+
+ private RSA _devicePrivateKey;
+ private ClientIdentification _identifierBlob;
+ private byte[] _identifier;
+ private byte[] _pssh;
+ private byte[] _rawLicenseRequest;
+ private byte[] _sessionKey;
+ private DerivedKeys _derivedKeys;
+ private OaepEncoding _decryptEngine;
+ public List ContentKeys { get; set; } = new List();
+ public dynamic InitData{ get; set; }
+
+ private AsymmetricCipherKeyPair DeviceKeys{ get; set; }
+
+ public Session(ContentDecryptionModule contentDecryptionModule, byte[] pssh){
+ _devicePrivateKey = CreatePrivateKeyFromPem(contentDecryptionModule.privateKey);
+
+ using var reader = new StringReader(Encoding.UTF8.GetString(contentDecryptionModule.privateKey));
+ DeviceKeys = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject();
+
+ _identifierBlob = Serializer.Deserialize(new MemoryStream(contentDecryptionModule.identifierBlob));
+ _identifier = GenerateIdentifier();
+ _pssh = pssh;
+ InitData = ParseInitData(pssh);
+ _decryptEngine = new OaepEncoding(new RsaEngine());
+ _decryptEngine.Init(false, DeviceKeys.Private);
+ }
+
+ private RSA CreatePrivateKeyFromPem(byte[] pemKey){
+ RSA rsa = RSA.Create();
+ string s = System.Text.Encoding.UTF8.GetString(pemKey);
+ rsa.ImportFromPem(s);
+ return rsa;
+ }
+
+ private byte[] GenerateIdentifier(){
+ // Generate 8 random bytes
+ byte[] randomBytes = RandomNumberGenerator.GetBytes(8);
+
+ // Convert to hex string
+ string hex = BitConverter.ToString(randomBytes).Replace("-", "").ToLower();
+
+ // Concatenate with '01' and '00000000000000'
+ string identifier = hex + "01" + "00000000000000";
+
+ // Convert the final string to a byte array
+ return Encoding.UTF8.GetBytes(identifier);
+ }
+
+ public byte[] GetLicenseRequest(){
+ dynamic licenseRequest;
+
+ if (InitData is WidevineCencHeader){
+ licenseRequest = new SignedLicenseRequest{
+ Type = SignedLicenseRequest.MessageType.LicenseRequest,
+ Msg = new LicenseRequest{
+ Type = LicenseRequest.RequestType.New,
+ KeyControlNonce = 1093602366,
+ ProtocolVersion = ProtocolVersion.Current,
+ RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString().Split(",")[0]),
+ ContentId = new LicenseRequest.ContentIdentification{
+ CencId = new LicenseRequest.ContentIdentification.Cenc{
+ LicenseType = LicenseType.Default,
+ RequestId = _identifier,
+ Pssh = InitData
+ }
+ }
+ }
+ };
+ } else{
+ licenseRequest = new SignedLicenseRequestRaw{
+ Type = SignedLicenseRequestRaw.MessageType.LicenseRequest,
+ Msg = new LicenseRequestRaw{
+ Type = LicenseRequestRaw.RequestType.New,
+ KeyControlNonce = 1093602366,
+ ProtocolVersion = ProtocolVersion.Current,
+ RequestTime = uint.Parse((DateTime.Now - DateTime.UnixEpoch).TotalSeconds.ToString().Split(",")[0]),
+ ContentId = new LicenseRequestRaw.ContentIdentification{
+ CencId = new LicenseRequestRaw.ContentIdentification.Cenc{
+ LicenseType = LicenseType.Default,
+ RequestId = _identifier,
+ Pssh = InitData
+ }
+ }
+ }
+ };
+ }
+
+ licenseRequest.Msg.ClientId = _identifierBlob;
+
+ //Logger.Debug("Signing license request");
+
+ using (var memoryStream = new MemoryStream()){
+ Serializer.Serialize(memoryStream, licenseRequest.Msg);
+ byte[] data = memoryStream.ToArray();
+ _rawLicenseRequest = data;
+
+ licenseRequest.Signature = Sign(data);
+ }
+
+ byte[] requestBytes;
+ using (var memoryStream = new MemoryStream()){
+ Serializer.Serialize(memoryStream, licenseRequest);
+ requestBytes = memoryStream.ToArray();
+ }
+
+ return requestBytes;
+ }
+
+ static WidevineCencHeader ParseInitData(byte[] initData){
+ WidevineCencHeader cencHeader;
+
+ try{
+ cencHeader = Serializer.Deserialize(new MemoryStream(initData[32..]));
+ } catch{
+ try{
+ //needed for HBO Max
+
+ PSSHBox psshBox = PSSHBox.FromByteArray(initData);
+ cencHeader = Serializer.Deserialize(new MemoryStream(psshBox.Data));
+ } catch{
+ //Logger.Verbose("Unable to parse, unsupported init data format");
+ return null;
+ }
+ }
+
+ return cencHeader;
+ }
+
+
+ public byte[] Sign(byte[] data){
+ PssSigner eng = new PssSigner(new RsaEngine(), new Sha1Digest());
+
+ eng.Init(true, DeviceKeys.Private);
+ eng.BlockUpdate(data, 0, data.Length);
+ return eng.GenerateSignature();
+ }
+
+ public byte[] Decrypt(byte[] data){
+ int blockSize = _decryptEngine.GetInputBlockSize();
+ List plainText = new List();
+
+ // Process the data in blocks
+ for (int chunkPosition = 0; chunkPosition < data.Length; chunkPosition += blockSize){
+ int chunkSize = Math.Min(blockSize, data.Length - chunkPosition);
+ byte[] decryptedChunk = _decryptEngine.ProcessBlock(data, chunkPosition, chunkSize);
+ plainText.AddRange(decryptedChunk);
+ }
+
+ return plainText.ToArray();
+ }
+
+ public void ProvideLicense(byte[] license){
+ SignedLicense signedLicense;
+ try{
+ signedLicense = Serializer.Deserialize(new MemoryStream(license));
+ } catch{
+ throw new Exception("Unable to parse license");
+ }
+
+ try{
+ var sessionKey = Decrypt(signedLicense.SessionKey);
+
+ if (sessionKey.Length != 16){
+ throw new Exception("Unable to decrypt session key");
+ }
+
+ _sessionKey = sessionKey;
+ } catch{
+ throw new Exception("Unable to decrypt session key");
+ }
+
+ _derivedKeys = DeriveKeys(_rawLicenseRequest, _sessionKey);
+
+ byte[] licenseBytes;
+ using (var memoryStream = new MemoryStream()){
+ Serializer.Serialize(memoryStream, signedLicense.Msg);
+ licenseBytes = memoryStream.ToArray();
+ }
+
+ byte[] hmacHash = CryptoUtils.GetHMACSHA256Digest(licenseBytes, _derivedKeys.Auth1);
+
+ if (!hmacHash.SequenceEqual(signedLicense.Signature)){
+ throw new Exception("License signature mismatch");
+ }
+
+ foreach (License.KeyContainer key in signedLicense.Msg.Keys){
+ string type = key.Type.ToString();
+
+ if (type == "Signing")
+ continue;
+
+ byte[] keyId;
+ byte[] encryptedKey = key.Key;
+ byte[] iv = key.Iv;
+ keyId = key.Id;
+ if (keyId == null){
+ keyId = Encoding.ASCII.GetBytes(key.Type.ToString());
+ }
+
+ byte[] decryptedKey;
+
+ using MemoryStream mstream = new MemoryStream();
+ using AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider{
+ Mode = CipherMode.CBC,
+ Padding = PaddingMode.PKCS7
+ };
+ using CryptoStream cryptoStream = new CryptoStream(mstream, aesProvider.CreateDecryptor(_derivedKeys.Enc, iv), CryptoStreamMode.Write);
+ cryptoStream.Write(encryptedKey, 0, encryptedKey.Length);
+ decryptedKey = mstream.ToArray();
+
+ List permissions = new List();
+ if (type == "OperatorSession"){
+ foreach (PropertyInfo perm in key._OperatorSessionKeyPermissions.GetType().GetProperties()){
+ if ((uint)perm.GetValue(key._OperatorSessionKeyPermissions) == 1){
+ permissions.Add(perm.Name);
+ }
+ }
+ }
+
+ ContentKeys.Add(new ContentKey{
+ KeyID = keyId,
+ Type = type,
+ Bytes = decryptedKey,
+ Permissions = permissions
+ });
+ }
+
+
+ }
+
+ public static DerivedKeys DeriveKeys(byte[] message, byte[] key){
+ byte[] encKeyBase = Encoding.UTF8.GetBytes("ENCRYPTION").Concat(new byte[]{ 0x0, }).Concat(message).Concat(new byte[]{ 0x0, 0x0, 0x0, 0x80 }).ToArray();
+ byte[] authKeyBase = Encoding.UTF8.GetBytes("AUTHENTICATION").Concat(new byte[]{ 0x0, }).Concat(message).Concat(new byte[]{ 0x0, 0x0, 0x2, 0x0 }).ToArray();
+
+ byte[] encKey = new byte[]{ 0x01 }.Concat(encKeyBase).ToArray();
+ byte[] authKey1 = new byte[]{ 0x01 }.Concat(authKeyBase).ToArray();
+ byte[] authKey2 = new byte[]{ 0x02 }.Concat(authKeyBase).ToArray();
+ byte[] authKey3 = new byte[]{ 0x03 }.Concat(authKeyBase).ToArray();
+ byte[] authKey4 = new byte[]{ 0x04 }.Concat(authKeyBase).ToArray();
+
+ byte[] encCmacKey = CryptoUtils.GetCMACDigest(encKey, key);
+ byte[] authCmacKey1 = CryptoUtils.GetCMACDigest(authKey1, key);
+ byte[] authCmacKey2 = CryptoUtils.GetCMACDigest(authKey2, key);
+ byte[] authCmacKey3 = CryptoUtils.GetCMACDigest(authKey3, key);
+ byte[] authCmacKey4 = CryptoUtils.GetCMACDigest(authKey4, key);
+
+ byte[] authCmacCombined1 = authCmacKey1.Concat(authCmacKey2).ToArray();
+ byte[] authCmacCombined2 = authCmacKey3.Concat(authCmacKey4).ToArray();
+
+ return new DerivedKeys{
+ Auth1 = authCmacCombined1,
+ Auth2 = authCmacCombined2,
+ Enc = encCmacKey
+ };
+ }
+
+ // public KeyContainer ParseLicense(byte[] rawLicense){
+ // if (_rawLicenseRequest == null){
+ // throw new InvalidOperationException("Please request a license first.");
+ // }
+ //
+ // // Assuming SignedMessage and License have Decode methods that deserialize the respective types
+ // var signedLicense = Serializer.Deserialize(new MemoryStream(rawLicense));
+ // byte[] sessionKey = _devicePrivateKey.Decrypt(signedLicense.SessionKey, RSAEncryptionPadding.OaepSHA256);
+ //
+ // var cmac = new AesCmac(sessionKey);
+ // var encKeyBase = Concat("ENCRYPTION\x00", _rawLicenseRequest, "\x00\x00\x00\x80");
+ // var authKeyBase = Concat("AUTHENTICATION\x00", _rawLicenseRequest, "\x00\x00\x02\x00");
+ //
+ // byte[] encKey = cmac.ComputeHash(Concat("\x01", encKeyBase));
+ // byte[] serverKey = Concat(
+ // cmac.ComputeHash(Concat("\x01", authKeyBase)),
+ // cmac.ComputeHash(Concat("\x02", authKeyBase))
+ // );
+ //
+ // using var hmac = new HMACSHA256(serverKey);
+ // byte[] calculatedSignature = hmac.ComputeHash(signedLicense.Msg);
+ //
+ // if (!calculatedSignature.SequenceEqual(signedLicense.Signature)){
+ // throw new InvalidOperationException("Signatures do not match.");
+ // }
+ //
+ // var license = License.Decode(signedLicense.Msg);
+ //
+ // return license.Key.Select(keyContainer => {
+ // string keyId = keyContainer.Id.Length > 0 ? BitConverter.ToString(keyContainer.Id).Replace("-", "").ToLower() : keyContainer.Type.ToString();
+ // using var aes = Aes.Create();
+ // aes.Key = encKey;
+ // aes.IV = keyContainer.Iv;
+ // aes.Mode = CipherMode.CBC;
+ //
+ // using var decryptor = aes.CreateDecryptor();
+ // byte[] decryptedKey = decryptor.TransformFinalBlock(keyContainer.Key, 0, keyContainer.Key.Length);
+ //
+ // return new KeyContainer{
+ // Kid = keyId,
+ // Key = BitConverter.ToString(decryptedKey).Replace("-", "").ToLower()
+ // };
+ // }).ToArray();
+ // }
+}
\ No newline at end of file
diff --git a/Utils/DRM/Widevine.cs b/Utils/DRM/Widevine.cs
new file mode 100644
index 0000000..5f6ba47
--- /dev/null
+++ b/Utils/DRM/Widevine.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace CRD.Utils.DRM;
+
+public class Widevine{
+ private byte[] privateKey = new byte[0];
+ private byte[] identifierBlob = new byte[0];
+
+ public bool canDecrypt = false;
+
+
+ #region Singelton
+
+ private static Widevine? instance;
+ private static readonly object padlock = new object();
+
+ public static Widevine Instance{
+ get{
+ if (instance == null){
+ lock (padlock){
+ if (instance == null){
+ instance = new Widevine();
+ }
+ }
+ }
+
+ return instance;
+ }
+ }
+
+ #endregion
+
+ public Widevine(){
+ try{
+ if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){
+ var files = Directory.GetFiles(CfgManager.PathWIDEVINE_DIR);
+
+ foreach (var file in files){
+ var fileInfo = new FileInfo(file);
+ if (fileInfo.Length < 1024 * 8 && !fileInfo.Attributes.HasFlag(FileAttributes.Directory)){
+ string fileContents = File.ReadAllText(file, Encoding.UTF8);
+ if (fileContents.Contains("-BEGIN RSA PRIVATE KEY-")){
+ privateKey = File.ReadAllBytes(file);
+ }
+
+ if (fileContents.Contains("widevine_cdm_version")){
+ identifierBlob = File.ReadAllBytes(file);
+ }
+ }
+ }
+ }
+
+
+ if (privateKey.Length != 0 && identifierBlob.Length != 0){
+ canDecrypt = true;
+ } else if (privateKey.Length == 0){
+ Console.WriteLine("Private key missing");
+ canDecrypt = false;
+ } else if (identifierBlob.Length == 0){
+ Console.WriteLine("Identifier blob missing");
+ canDecrypt = false;
+ }
+ } catch (Exception e){
+ Console.WriteLine(e);
+ canDecrypt = false;
+ }
+ }
+
+ public async Task> getKeys(string? pssh, string licenseServer, Dictionary authData){
+ if (pssh == null || !canDecrypt) return new List();
+
+ byte[] psshBuffer = Convert.FromBase64String(pssh);
+
+ Session ses = new Session(new ContentDecryptionModule{ identifierBlob = identifierBlob, privateKey = privateKey }, psshBuffer);
+
+ var playbackRequest2 = new HttpRequestMessage(HttpMethod.Post, licenseServer);
+ foreach (var keyValuePair in authData){
+ playbackRequest2.Headers.Add(keyValuePair.Key, keyValuePair.Value);
+ }
+
+ var licenceReq = ses.GetLicenseRequest();
+ playbackRequest2.Content = new ByteArrayContent(licenceReq);
+
+ var response = await HttpClientReq.Instance.SendHttpRequest(playbackRequest2);
+
+ if (!response.IsOk){
+ Console.WriteLine("Fallback Request Stream URLs FAILED!");
+ return new List();
+ }
+
+ LicenceReqResp resp = Helpers.Deserialize(response.ResponseContent,null) ?? new LicenceReqResp();
+
+ ses.ProvideLicense(Convert.FromBase64String(resp.license));
+
+ return ses.ContentKeys;
+ }
+}
+
+public class LicenceReqResp{
+ public string status{ get; set; }
+ public string license{ get; set; }
+ public string platform{ get; set; }
+ public string message_type{ get; set; }
+}
\ No newline at end of file
diff --git a/Utils/DRM/WvProto2.cs b/Utils/DRM/WvProto2.cs
new file mode 100644
index 0000000..342221e
--- /dev/null
+++ b/Utils/DRM/WvProto2.cs
@@ -0,0 +1,2259 @@
+namespace CRD.Utils.DRM;
+
+//
+// This file was generated by a tool; you should avoid making direct changes.
+// Consider using 'partial classes' to extend these types
+// Input: my.proto
+//
+
+#region Designer generated code
+#pragma warning disable CS0612, CS0618, CS1591, CS3021, IDE0079, IDE1006, RCS1036, RCS1057, RCS1085, RCS1192
+[global::ProtoBuf.ProtoContract()]
+public partial class ClientIdentification : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, IsRequired = true)]
+ public TokenType Type { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public SignedDeviceCertificate Token { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3, Name = @"ClientInfo")]
+ public global::System.Collections.Generic.List ClientInfoes { get; } = new global::System.Collections.Generic.List();
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] ProviderClientToken
+ {
+ get => __pbn__ProviderClientToken;
+ set => __pbn__ProviderClientToken = value;
+ }
+ public bool ShouldSerializeProviderClientToken() => __pbn__ProviderClientToken != null;
+ public void ResetProviderClientToken() => __pbn__ProviderClientToken = null;
+ private byte[] __pbn__ProviderClientToken;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public uint LicenseCounter
+ {
+ get => __pbn__LicenseCounter.GetValueOrDefault();
+ set => __pbn__LicenseCounter = value;
+ }
+ public bool ShouldSerializeLicenseCounter() => __pbn__LicenseCounter != null;
+ public void ResetLicenseCounter() => __pbn__LicenseCounter = null;
+ private uint? __pbn__LicenseCounter;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public ClientCapabilities _ClientCapabilities { get; set; }
+
+ [global::ProtoBuf.ProtoMember(7, Name = @"_FileHashes")]
+ public FileHashes FileHashes { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class NameValue : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, IsRequired = true)]
+ public string Name { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2, IsRequired = true)]
+ public string Value { get; set; }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class ClientCapabilities : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public uint ClientToken
+ {
+ get => __pbn__ClientToken.GetValueOrDefault();
+ set => __pbn__ClientToken = value;
+ }
+ public bool ShouldSerializeClientToken() => __pbn__ClientToken != null;
+ public void ResetClientToken() => __pbn__ClientToken = null;
+ private uint? __pbn__ClientToken;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public uint SessionToken
+ {
+ get => __pbn__SessionToken.GetValueOrDefault();
+ set => __pbn__SessionToken = value;
+ }
+ public bool ShouldSerializeSessionToken() => __pbn__SessionToken != null;
+ public void ResetSessionToken() => __pbn__SessionToken = null;
+ private uint? __pbn__SessionToken;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public uint VideoResolutionConstraints
+ {
+ get => __pbn__VideoResolutionConstraints.GetValueOrDefault();
+ set => __pbn__VideoResolutionConstraints = value;
+ }
+ public bool ShouldSerializeVideoResolutionConstraints() => __pbn__VideoResolutionConstraints != null;
+ public void ResetVideoResolutionConstraints() => __pbn__VideoResolutionConstraints = null;
+ private uint? __pbn__VideoResolutionConstraints;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ [global::System.ComponentModel.DefaultValue(HdcpVersion.HdcpNone)]
+ public HdcpVersion MaxHdcpVersion
+ {
+ get => __pbn__MaxHdcpVersion ?? HdcpVersion.HdcpNone;
+ set => __pbn__MaxHdcpVersion = value;
+ }
+ public bool ShouldSerializeMaxHdcpVersion() => __pbn__MaxHdcpVersion != null;
+ public void ResetMaxHdcpVersion() => __pbn__MaxHdcpVersion = null;
+ private HdcpVersion? __pbn__MaxHdcpVersion;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public uint OemCryptoApiVersion
+ {
+ get => __pbn__OemCryptoApiVersion.GetValueOrDefault();
+ set => __pbn__OemCryptoApiVersion = value;
+ }
+ public bool ShouldSerializeOemCryptoApiVersion() => __pbn__OemCryptoApiVersion != null;
+ public void ResetOemCryptoApiVersion() => __pbn__OemCryptoApiVersion = null;
+ private uint? __pbn__OemCryptoApiVersion;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum HdcpVersion
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"HDCP_NONE")]
+ HdcpNone = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"HDCP_V1")]
+ HdcpV1 = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"HDCP_V2")]
+ HdcpV2 = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"HDCP_V2_1")]
+ HdcpV21 = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"HDCP_V2_2")]
+ HdcpV22 = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"HDCP_V2_3")]
+ HdcpV23 = 5,
+ }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum TokenType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"KEYBOX")]
+ Keybox = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"DEVICE_CERTIFICATE")]
+ DeviceCertificate = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"REMOTE_ATTESTATION_CERTIFICATE")]
+ RemoteAttestationCertificate = 2,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class DeviceCertificate : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, IsRequired = true)]
+ public CertificateType Type { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] SerialNumber
+ {
+ get => __pbn__SerialNumber;
+ set => __pbn__SerialNumber = value;
+ }
+ public bool ShouldSerializeSerialNumber() => __pbn__SerialNumber != null;
+ public void ResetSerialNumber() => __pbn__SerialNumber = null;
+ private byte[] __pbn__SerialNumber;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public uint CreationTimeSeconds
+ {
+ get => __pbn__CreationTimeSeconds.GetValueOrDefault();
+ set => __pbn__CreationTimeSeconds = value;
+ }
+ public bool ShouldSerializeCreationTimeSeconds() => __pbn__CreationTimeSeconds != null;
+ public void ResetCreationTimeSeconds() => __pbn__CreationTimeSeconds = null;
+ private uint? __pbn__CreationTimeSeconds;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] PublicKey
+ {
+ get => __pbn__PublicKey;
+ set => __pbn__PublicKey = value;
+ }
+ public bool ShouldSerializePublicKey() => __pbn__PublicKey != null;
+ public void ResetPublicKey() => __pbn__PublicKey = null;
+ private byte[] __pbn__PublicKey;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public uint SystemId
+ {
+ get => __pbn__SystemId.GetValueOrDefault();
+ set => __pbn__SystemId = value;
+ }
+ public bool ShouldSerializeSystemId() => __pbn__SystemId != null;
+ public void ResetSystemId() => __pbn__SystemId = null;
+ private uint? __pbn__SystemId;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public uint TestDeviceDeprecated
+ {
+ get => __pbn__TestDeviceDeprecated.GetValueOrDefault();
+ set => __pbn__TestDeviceDeprecated = value;
+ }
+ public bool ShouldSerializeTestDeviceDeprecated() => __pbn__TestDeviceDeprecated != null;
+ public void ResetTestDeviceDeprecated() => __pbn__TestDeviceDeprecated = null;
+ private uint? __pbn__TestDeviceDeprecated;
+
+ [global::ProtoBuf.ProtoMember(7)]
+ public byte[] ServiceId
+ {
+ get => __pbn__ServiceId;
+ set => __pbn__ServiceId = value;
+ }
+ public bool ShouldSerializeServiceId() => __pbn__ServiceId != null;
+ public void ResetServiceId() => __pbn__ServiceId = null;
+ private byte[] __pbn__ServiceId;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum CertificateType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"ROOT")]
+ Root = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"INTERMEDIATE")]
+ Intermediate = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"USER_DEVICE")]
+ UserDevice = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE")]
+ Service = 3,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class DeviceCertificateStatus : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public byte[] SerialNumber
+ {
+ get => __pbn__SerialNumber;
+ set => __pbn__SerialNumber = value;
+ }
+ public bool ShouldSerializeSerialNumber() => __pbn__SerialNumber != null;
+ public void ResetSerialNumber() => __pbn__SerialNumber = null;
+ private byte[] __pbn__SerialNumber;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue(CertificateStatus.Valid)]
+ public CertificateStatus Status
+ {
+ get => __pbn__Status ?? CertificateStatus.Valid;
+ set => __pbn__Status = value;
+ }
+ public bool ShouldSerializeStatus() => __pbn__Status != null;
+ public void ResetStatus() => __pbn__Status = null;
+ private CertificateStatus? __pbn__Status;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public ProvisionedDeviceInfo DeviceInfo { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum CertificateStatus
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"VALID")]
+ Valid = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"REVOKED")]
+ Revoked = 1,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class DeviceCertificateStatusList : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public uint CreationTimeSeconds
+ {
+ get => __pbn__CreationTimeSeconds.GetValueOrDefault();
+ set => __pbn__CreationTimeSeconds = value;
+ }
+ public bool ShouldSerializeCreationTimeSeconds() => __pbn__CreationTimeSeconds != null;
+ public void ResetCreationTimeSeconds() => __pbn__CreationTimeSeconds = null;
+ private uint? __pbn__CreationTimeSeconds;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public global::System.Collections.Generic.List CertificateStatus { get; } = new global::System.Collections.Generic.List();
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedDeviceCertificateStatusList : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public DeviceCertificateStatusList CertificateList { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class EncryptedClientIdentification : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, IsRequired = true)]
+ public string ServiceId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] ServiceCertificateSerialNumber
+ {
+ get => __pbn__ServiceCertificateSerialNumber;
+ set => __pbn__ServiceCertificateSerialNumber = value;
+ }
+ public bool ShouldSerializeServiceCertificateSerialNumber() => __pbn__ServiceCertificateSerialNumber != null;
+ public void ResetServiceCertificateSerialNumber() => __pbn__ServiceCertificateSerialNumber = null;
+ private byte[] __pbn__ServiceCertificateSerialNumber;
+
+ [global::ProtoBuf.ProtoMember(3, IsRequired = true)]
+ public byte[] EncryptedClientId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(4, IsRequired = true)]
+ public byte[] EncryptedClientIdIv { get; set; }
+
+ [global::ProtoBuf.ProtoMember(5, IsRequired = true)]
+ public byte[] EncryptedPrivacyKey { get; set; }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class LicenseIdentification : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public byte[] RequestId
+ {
+ get => __pbn__RequestId;
+ set => __pbn__RequestId = value;
+ }
+ public bool ShouldSerializeRequestId() => __pbn__RequestId != null;
+ public void ResetRequestId() => __pbn__RequestId = null;
+ private byte[] __pbn__RequestId;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] SessionId
+ {
+ get => __pbn__SessionId;
+ set => __pbn__SessionId = value;
+ }
+ public bool ShouldSerializeSessionId() => __pbn__SessionId != null;
+ public void ResetSessionId() => __pbn__SessionId = null;
+ private byte[] __pbn__SessionId;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] PurchaseId
+ {
+ get => __pbn__PurchaseId;
+ set => __pbn__PurchaseId = value;
+ }
+ public bool ShouldSerializePurchaseId() => __pbn__PurchaseId != null;
+ public void ResetPurchaseId() => __pbn__PurchaseId = null;
+ private byte[] __pbn__PurchaseId;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ [global::System.ComponentModel.DefaultValue(LicenseType.Zero)]
+ public LicenseType Type
+ {
+ get => __pbn__Type ?? LicenseType.Zero;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private LicenseType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public uint Version
+ {
+ get => __pbn__Version.GetValueOrDefault();
+ set => __pbn__Version = value;
+ }
+ public bool ShouldSerializeVersion() => __pbn__Version != null;
+ public void ResetVersion() => __pbn__Version = null;
+ private uint? __pbn__Version;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public byte[] ProviderSessionToken
+ {
+ get => __pbn__ProviderSessionToken;
+ set => __pbn__ProviderSessionToken = value;
+ }
+ public bool ShouldSerializeProviderSessionToken() => __pbn__ProviderSessionToken != null;
+ public void ResetProviderSessionToken() => __pbn__ProviderSessionToken = null;
+ private byte[] __pbn__ProviderSessionToken;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class License : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public LicenseIdentification Id { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public Policy _Policy { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3, Name = @"Key")]
+ public global::System.Collections.Generic.List Keys { get; } = new global::System.Collections.Generic.List();
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public uint LicenseStartTime
+ {
+ get => __pbn__LicenseStartTime.GetValueOrDefault();
+ set => __pbn__LicenseStartTime = value;
+ }
+ public bool ShouldSerializeLicenseStartTime() => __pbn__LicenseStartTime != null;
+ public void ResetLicenseStartTime() => __pbn__LicenseStartTime = null;
+ private uint? __pbn__LicenseStartTime;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public uint RemoteAttestationVerified
+ {
+ get => __pbn__RemoteAttestationVerified.GetValueOrDefault();
+ set => __pbn__RemoteAttestationVerified = value;
+ }
+ public bool ShouldSerializeRemoteAttestationVerified() => __pbn__RemoteAttestationVerified != null;
+ public void ResetRemoteAttestationVerified() => __pbn__RemoteAttestationVerified = null;
+ private uint? __pbn__RemoteAttestationVerified;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public byte[] ProviderClientToken
+ {
+ get => __pbn__ProviderClientToken;
+ set => __pbn__ProviderClientToken = value;
+ }
+ public bool ShouldSerializeProviderClientToken() => __pbn__ProviderClientToken != null;
+ public void ResetProviderClientToken() => __pbn__ProviderClientToken = null;
+ private byte[] __pbn__ProviderClientToken;
+
+ [global::ProtoBuf.ProtoMember(7)]
+ public uint ProtectionScheme
+ {
+ get => __pbn__ProtectionScheme.GetValueOrDefault();
+ set => __pbn__ProtectionScheme = value;
+ }
+ public bool ShouldSerializeProtectionScheme() => __pbn__ProtectionScheme != null;
+ public void ResetProtectionScheme() => __pbn__ProtectionScheme = null;
+ private uint? __pbn__ProtectionScheme;
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class Policy : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public bool CanPlay
+ {
+ get => __pbn__CanPlay.GetValueOrDefault();
+ set => __pbn__CanPlay = value;
+ }
+ public bool ShouldSerializeCanPlay() => __pbn__CanPlay != null;
+ public void ResetCanPlay() => __pbn__CanPlay = null;
+ private bool? __pbn__CanPlay;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public bool CanPersist
+ {
+ get => __pbn__CanPersist.GetValueOrDefault();
+ set => __pbn__CanPersist = value;
+ }
+ public bool ShouldSerializeCanPersist() => __pbn__CanPersist != null;
+ public void ResetCanPersist() => __pbn__CanPersist = null;
+ private bool? __pbn__CanPersist;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public bool CanRenew
+ {
+ get => __pbn__CanRenew.GetValueOrDefault();
+ set => __pbn__CanRenew = value;
+ }
+ public bool ShouldSerializeCanRenew() => __pbn__CanRenew != null;
+ public void ResetCanRenew() => __pbn__CanRenew = null;
+ private bool? __pbn__CanRenew;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public uint RentalDurationSeconds
+ {
+ get => __pbn__RentalDurationSeconds.GetValueOrDefault();
+ set => __pbn__RentalDurationSeconds = value;
+ }
+ public bool ShouldSerializeRentalDurationSeconds() => __pbn__RentalDurationSeconds != null;
+ public void ResetRentalDurationSeconds() => __pbn__RentalDurationSeconds = null;
+ private uint? __pbn__RentalDurationSeconds;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public uint PlaybackDurationSeconds
+ {
+ get => __pbn__PlaybackDurationSeconds.GetValueOrDefault();
+ set => __pbn__PlaybackDurationSeconds = value;
+ }
+ public bool ShouldSerializePlaybackDurationSeconds() => __pbn__PlaybackDurationSeconds != null;
+ public void ResetPlaybackDurationSeconds() => __pbn__PlaybackDurationSeconds = null;
+ private uint? __pbn__PlaybackDurationSeconds;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public uint LicenseDurationSeconds
+ {
+ get => __pbn__LicenseDurationSeconds.GetValueOrDefault();
+ set => __pbn__LicenseDurationSeconds = value;
+ }
+ public bool ShouldSerializeLicenseDurationSeconds() => __pbn__LicenseDurationSeconds != null;
+ public void ResetLicenseDurationSeconds() => __pbn__LicenseDurationSeconds = null;
+ private uint? __pbn__LicenseDurationSeconds;
+
+ [global::ProtoBuf.ProtoMember(7)]
+ public uint RenewalRecoveryDurationSeconds
+ {
+ get => __pbn__RenewalRecoveryDurationSeconds.GetValueOrDefault();
+ set => __pbn__RenewalRecoveryDurationSeconds = value;
+ }
+ public bool ShouldSerializeRenewalRecoveryDurationSeconds() => __pbn__RenewalRecoveryDurationSeconds != null;
+ public void ResetRenewalRecoveryDurationSeconds() => __pbn__RenewalRecoveryDurationSeconds = null;
+ private uint? __pbn__RenewalRecoveryDurationSeconds;
+
+ [global::ProtoBuf.ProtoMember(8)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string RenewalServerUrl
+ {
+ get => __pbn__RenewalServerUrl ?? "";
+ set => __pbn__RenewalServerUrl = value;
+ }
+ public bool ShouldSerializeRenewalServerUrl() => __pbn__RenewalServerUrl != null;
+ public void ResetRenewalServerUrl() => __pbn__RenewalServerUrl = null;
+ private string __pbn__RenewalServerUrl;
+
+ [global::ProtoBuf.ProtoMember(9)]
+ public uint RenewalDelaySeconds
+ {
+ get => __pbn__RenewalDelaySeconds.GetValueOrDefault();
+ set => __pbn__RenewalDelaySeconds = value;
+ }
+ public bool ShouldSerializeRenewalDelaySeconds() => __pbn__RenewalDelaySeconds != null;
+ public void ResetRenewalDelaySeconds() => __pbn__RenewalDelaySeconds = null;
+ private uint? __pbn__RenewalDelaySeconds;
+
+ [global::ProtoBuf.ProtoMember(10)]
+ public uint RenewalRetryIntervalSeconds
+ {
+ get => __pbn__RenewalRetryIntervalSeconds.GetValueOrDefault();
+ set => __pbn__RenewalRetryIntervalSeconds = value;
+ }
+ public bool ShouldSerializeRenewalRetryIntervalSeconds() => __pbn__RenewalRetryIntervalSeconds != null;
+ public void ResetRenewalRetryIntervalSeconds() => __pbn__RenewalRetryIntervalSeconds = null;
+ private uint? __pbn__RenewalRetryIntervalSeconds;
+
+ [global::ProtoBuf.ProtoMember(11)]
+ public bool RenewWithUsage
+ {
+ get => __pbn__RenewWithUsage.GetValueOrDefault();
+ set => __pbn__RenewWithUsage = value;
+ }
+ public bool ShouldSerializeRenewWithUsage() => __pbn__RenewWithUsage != null;
+ public void ResetRenewWithUsage() => __pbn__RenewWithUsage = null;
+ private bool? __pbn__RenewWithUsage;
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class KeyContainer : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public byte[] Id
+ {
+ get => __pbn__Id;
+ set => __pbn__Id = value;
+ }
+ public bool ShouldSerializeId() => __pbn__Id != null;
+ public void ResetId() => __pbn__Id = null;
+ private byte[] __pbn__Id;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] Iv
+ {
+ get => __pbn__Iv;
+ set => __pbn__Iv = value;
+ }
+ public bool ShouldSerializeIv() => __pbn__Iv != null;
+ public void ResetIv() => __pbn__Iv = null;
+ private byte[] __pbn__Iv;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] Key
+ {
+ get => __pbn__Key;
+ set => __pbn__Key = value;
+ }
+ public bool ShouldSerializeKey() => __pbn__Key != null;
+ public void ResetKey() => __pbn__Key = null;
+ private byte[] __pbn__Key;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ [global::System.ComponentModel.DefaultValue(KeyType.Signing)]
+ public KeyType Type
+ {
+ get => __pbn__Type ?? KeyType.Signing;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private KeyType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ [global::System.ComponentModel.DefaultValue(SecurityLevel.SwSecureCrypto)]
+ public SecurityLevel Level
+ {
+ get => __pbn__Level ?? SecurityLevel.SwSecureCrypto;
+ set => __pbn__Level = value;
+ }
+ public bool ShouldSerializeLevel() => __pbn__Level != null;
+ public void ResetLevel() => __pbn__Level = null;
+ private SecurityLevel? __pbn__Level;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public OutputProtection RequiredProtection { get; set; }
+
+ [global::ProtoBuf.ProtoMember(7)]
+ public OutputProtection RequestedProtection { get; set; }
+
+ [global::ProtoBuf.ProtoMember(8)]
+ public KeyControl _KeyControl { get; set; }
+
+ [global::ProtoBuf.ProtoMember(9)]
+ public OperatorSessionKeyPermissions _OperatorSessionKeyPermissions { get; set; }
+
+ [global::ProtoBuf.ProtoMember(10)]
+ public global::System.Collections.Generic.List VideoResolutionConstraints { get; } = new global::System.Collections.Generic.List();
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class OutputProtection : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(ClientIdentification.ClientCapabilities.HdcpVersion.HdcpNone)]
+ public ClientIdentification.ClientCapabilities.HdcpVersion Hdcp
+ {
+ get => __pbn__Hdcp ?? ClientIdentification.ClientCapabilities.HdcpVersion.HdcpNone;
+ set => __pbn__Hdcp = value;
+ }
+ public bool ShouldSerializeHdcp() => __pbn__Hdcp != null;
+ public void ResetHdcp() => __pbn__Hdcp = null;
+ private ClientIdentification.ClientCapabilities.HdcpVersion? __pbn__Hdcp;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue(Cgms.CopyFree)]
+ public Cgms CgmsFlags
+ {
+ get => __pbn__CgmsFlags ?? Cgms.CopyFree;
+ set => __pbn__CgmsFlags = value;
+ }
+ public bool ShouldSerializeCgmsFlags() => __pbn__CgmsFlags != null;
+ public void ResetCgmsFlags() => __pbn__CgmsFlags = null;
+ private Cgms? __pbn__CgmsFlags;
+
+ [global::ProtoBuf.ProtoContract(Name = @"CGMS")]
+ public enum Cgms
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"COPY_FREE")]
+ CopyFree = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"COPY_ONCE")]
+ CopyOnce = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"COPY_NEVER")]
+ CopyNever = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"CGMS_NONE")]
+ CgmsNone = 42,
+ }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class KeyControl : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, IsRequired = true)]
+ public byte[] KeyControlBlock { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2, IsRequired = true)]
+ public byte[] Iv { get; set; }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class OperatorSessionKeyPermissions : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public uint AllowEncrypt
+ {
+ get => __pbn__AllowEncrypt.GetValueOrDefault();
+ set => __pbn__AllowEncrypt = value;
+ }
+ public bool ShouldSerializeAllowEncrypt() => __pbn__AllowEncrypt != null;
+ public void ResetAllowEncrypt() => __pbn__AllowEncrypt = null;
+ private uint? __pbn__AllowEncrypt;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public uint AllowDecrypt
+ {
+ get => __pbn__AllowDecrypt.GetValueOrDefault();
+ set => __pbn__AllowDecrypt = value;
+ }
+ public bool ShouldSerializeAllowDecrypt() => __pbn__AllowDecrypt != null;
+ public void ResetAllowDecrypt() => __pbn__AllowDecrypt = null;
+ private uint? __pbn__AllowDecrypt;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public uint AllowSign
+ {
+ get => __pbn__AllowSign.GetValueOrDefault();
+ set => __pbn__AllowSign = value;
+ }
+ public bool ShouldSerializeAllowSign() => __pbn__AllowSign != null;
+ public void ResetAllowSign() => __pbn__AllowSign = null;
+ private uint? __pbn__AllowSign;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public uint AllowSignatureVerify
+ {
+ get => __pbn__AllowSignatureVerify.GetValueOrDefault();
+ set => __pbn__AllowSignatureVerify = value;
+ }
+ public bool ShouldSerializeAllowSignatureVerify() => __pbn__AllowSignatureVerify != null;
+ public void ResetAllowSignatureVerify() => __pbn__AllowSignatureVerify = null;
+ private uint? __pbn__AllowSignatureVerify;
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class VideoResolutionConstraint : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public uint MinResolutionPixels
+ {
+ get => __pbn__MinResolutionPixels.GetValueOrDefault();
+ set => __pbn__MinResolutionPixels = value;
+ }
+ public bool ShouldSerializeMinResolutionPixels() => __pbn__MinResolutionPixels != null;
+ public void ResetMinResolutionPixels() => __pbn__MinResolutionPixels = null;
+ private uint? __pbn__MinResolutionPixels;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public uint MaxResolutionPixels
+ {
+ get => __pbn__MaxResolutionPixels.GetValueOrDefault();
+ set => __pbn__MaxResolutionPixels = value;
+ }
+ public bool ShouldSerializeMaxResolutionPixels() => __pbn__MaxResolutionPixels != null;
+ public void ResetMaxResolutionPixels() => __pbn__MaxResolutionPixels = null;
+ private uint? __pbn__MaxResolutionPixels;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public License.KeyContainer.OutputProtection RequiredProtection { get; set; }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum KeyType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"SIGNING")]
+ Signing = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"CONTENT")]
+ Content = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"KEY_CONTROL")]
+ KeyControl = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"OPERATOR_SESSION")]
+ OperatorSession = 4,
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum SecurityLevel
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"SW_SECURE_CRYPTO")]
+ SwSecureCrypto = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"SW_SECURE_DECODE")]
+ SwSecureDecode = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"HW_SECURE_CRYPTO")]
+ HwSecureCrypto = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"HW_SECURE_DECODE")]
+ HwSecureDecode = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"HW_SECURE_ALL")]
+ HwSecureAll = 5,
+ }
+
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class LicenseError : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(Error.InvalidDeviceCertificate)]
+ public Error ErrorCode
+ {
+ get => __pbn__ErrorCode ?? Error.InvalidDeviceCertificate;
+ set => __pbn__ErrorCode = value;
+ }
+ public bool ShouldSerializeErrorCode() => __pbn__ErrorCode != null;
+ public void ResetErrorCode() => __pbn__ErrorCode = null;
+ private Error? __pbn__ErrorCode;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum Error
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"INVALID_DEVICE_CERTIFICATE")]
+ InvalidDeviceCertificate = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"REVOKED_DEVICE_CERTIFICATE")]
+ RevokedDeviceCertificate = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_UNAVAILABLE")]
+ ServiceUnavailable = 3,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class LicenseRequest : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public ClientIdentification ClientId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public ContentIdentification ContentId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ [global::System.ComponentModel.DefaultValue(RequestType.New)]
+ public RequestType Type
+ {
+ get => __pbn__Type ?? RequestType.New;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private RequestType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public uint RequestTime
+ {
+ get => __pbn__RequestTime.GetValueOrDefault();
+ set => __pbn__RequestTime = value;
+ }
+ public bool ShouldSerializeRequestTime() => __pbn__RequestTime != null;
+ public void ResetRequestTime() => __pbn__RequestTime = null;
+ private uint? __pbn__RequestTime;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public byte[] KeyControlNonceDeprecated
+ {
+ get => __pbn__KeyControlNonceDeprecated;
+ set => __pbn__KeyControlNonceDeprecated = value;
+ }
+ public bool ShouldSerializeKeyControlNonceDeprecated() => __pbn__KeyControlNonceDeprecated != null;
+ public void ResetKeyControlNonceDeprecated() => __pbn__KeyControlNonceDeprecated = null;
+ private byte[] __pbn__KeyControlNonceDeprecated;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ [global::System.ComponentModel.DefaultValue(ProtocolVersion.Current)]
+ public ProtocolVersion ProtocolVersion
+ {
+ get => __pbn__ProtocolVersion ?? ProtocolVersion.Current;
+ set => __pbn__ProtocolVersion = value;
+ }
+ public bool ShouldSerializeProtocolVersion() => __pbn__ProtocolVersion != null;
+ public void ResetProtocolVersion() => __pbn__ProtocolVersion = null;
+ private ProtocolVersion? __pbn__ProtocolVersion;
+
+ [global::ProtoBuf.ProtoMember(7)]
+ public uint KeyControlNonce
+ {
+ get => __pbn__KeyControlNonce.GetValueOrDefault();
+ set => __pbn__KeyControlNonce = value;
+ }
+ public bool ShouldSerializeKeyControlNonce() => __pbn__KeyControlNonce != null;
+ public void ResetKeyControlNonce() => __pbn__KeyControlNonce = null;
+ private uint? __pbn__KeyControlNonce;
+
+ [global::ProtoBuf.ProtoMember(8)]
+ public EncryptedClientIdentification EncryptedClientId { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class ContentIdentification : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public Cenc CencId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public WebM WebmId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public ExistingLicense License { get; set; }
+
+ [global::ProtoBuf.ProtoContract(Name = @"CENC")]
+ public partial class Cenc : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public WidevineCencHeader Pssh { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue(LicenseType.Zero)]
+ public LicenseType LicenseType
+ {
+ get => __pbn__LicenseType ?? LicenseType.Zero;
+ set => __pbn__LicenseType = value;
+ }
+ public bool ShouldSerializeLicenseType() => __pbn__LicenseType != null;
+ public void ResetLicenseType() => __pbn__LicenseType = null;
+ private LicenseType? __pbn__LicenseType;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] RequestId
+ {
+ get => __pbn__RequestId;
+ set => __pbn__RequestId = value;
+ }
+ public bool ShouldSerializeRequestId() => __pbn__RequestId != null;
+ public void ResetRequestId() => __pbn__RequestId = null;
+ private byte[] __pbn__RequestId;
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class WebM : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public byte[] Header
+ {
+ get => __pbn__Header;
+ set => __pbn__Header = value;
+ }
+ public bool ShouldSerializeHeader() => __pbn__Header != null;
+ public void ResetHeader() => __pbn__Header = null;
+ private byte[] __pbn__Header;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue(LicenseType.Zero)]
+ public LicenseType LicenseType
+ {
+ get => __pbn__LicenseType ?? LicenseType.Zero;
+ set => __pbn__LicenseType = value;
+ }
+ public bool ShouldSerializeLicenseType() => __pbn__LicenseType != null;
+ public void ResetLicenseType() => __pbn__LicenseType = null;
+ private LicenseType? __pbn__LicenseType;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] RequestId
+ {
+ get => __pbn__RequestId;
+ set => __pbn__RequestId = value;
+ }
+ public bool ShouldSerializeRequestId() => __pbn__RequestId != null;
+ public void ResetRequestId() => __pbn__RequestId = null;
+ private byte[] __pbn__RequestId;
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class ExistingLicense : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public LicenseIdentification LicenseId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public uint SecondsSinceStarted
+ {
+ get => __pbn__SecondsSinceStarted.GetValueOrDefault();
+ set => __pbn__SecondsSinceStarted = value;
+ }
+ public bool ShouldSerializeSecondsSinceStarted() => __pbn__SecondsSinceStarted != null;
+ public void ResetSecondsSinceStarted() => __pbn__SecondsSinceStarted = null;
+ private uint? __pbn__SecondsSinceStarted;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public uint SecondsSinceLastPlayed
+ {
+ get => __pbn__SecondsSinceLastPlayed.GetValueOrDefault();
+ set => __pbn__SecondsSinceLastPlayed = value;
+ }
+ public bool ShouldSerializeSecondsSinceLastPlayed() => __pbn__SecondsSinceLastPlayed != null;
+ public void ResetSecondsSinceLastPlayed() => __pbn__SecondsSinceLastPlayed = null;
+ private uint? __pbn__SecondsSinceLastPlayed;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionUsageTableEntry
+ {
+ get => __pbn__SessionUsageTableEntry;
+ set => __pbn__SessionUsageTableEntry = value;
+ }
+ public bool ShouldSerializeSessionUsageTableEntry() => __pbn__SessionUsageTableEntry != null;
+ public void ResetSessionUsageTableEntry() => __pbn__SessionUsageTableEntry = null;
+ private byte[] __pbn__SessionUsageTableEntry;
+
+ }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum RequestType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"NEW")]
+ New = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"RENEWAL")]
+ Renewal = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"RELEASE")]
+ Release = 3,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class LicenseRequestRaw : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public ClientIdentification ClientId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public ContentIdentification ContentId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ [global::System.ComponentModel.DefaultValue(RequestType.New)]
+ public RequestType Type
+ {
+ get => __pbn__Type ?? RequestType.New;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private RequestType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public uint RequestTime
+ {
+ get => __pbn__RequestTime.GetValueOrDefault();
+ set => __pbn__RequestTime = value;
+ }
+ public bool ShouldSerializeRequestTime() => __pbn__RequestTime != null;
+ public void ResetRequestTime() => __pbn__RequestTime = null;
+ private uint? __pbn__RequestTime;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public byte[] KeyControlNonceDeprecated
+ {
+ get => __pbn__KeyControlNonceDeprecated;
+ set => __pbn__KeyControlNonceDeprecated = value;
+ }
+ public bool ShouldSerializeKeyControlNonceDeprecated() => __pbn__KeyControlNonceDeprecated != null;
+ public void ResetKeyControlNonceDeprecated() => __pbn__KeyControlNonceDeprecated = null;
+ private byte[] __pbn__KeyControlNonceDeprecated;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ [global::System.ComponentModel.DefaultValue(ProtocolVersion.Current)]
+ public ProtocolVersion ProtocolVersion
+ {
+ get => __pbn__ProtocolVersion ?? ProtocolVersion.Current;
+ set => __pbn__ProtocolVersion = value;
+ }
+ public bool ShouldSerializeProtocolVersion() => __pbn__ProtocolVersion != null;
+ public void ResetProtocolVersion() => __pbn__ProtocolVersion = null;
+ private ProtocolVersion? __pbn__ProtocolVersion;
+
+ [global::ProtoBuf.ProtoMember(7)]
+ public uint KeyControlNonce
+ {
+ get => __pbn__KeyControlNonce.GetValueOrDefault();
+ set => __pbn__KeyControlNonce = value;
+ }
+ public bool ShouldSerializeKeyControlNonce() => __pbn__KeyControlNonce != null;
+ public void ResetKeyControlNonce() => __pbn__KeyControlNonce = null;
+ private uint? __pbn__KeyControlNonce;
+
+ [global::ProtoBuf.ProtoMember(8)]
+ public EncryptedClientIdentification EncryptedClientId { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class ContentIdentification : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public Cenc CencId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public WebM WebmId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public ExistingLicense License { get; set; }
+
+ [global::ProtoBuf.ProtoContract(Name = @"CENC")]
+ public partial class Cenc : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public byte[] Pssh
+ {
+ get => __pbn__Pssh;
+ set => __pbn__Pssh = value;
+ }
+ public bool ShouldSerializePssh() => __pbn__Pssh != null;
+ public void ResetPssh() => __pbn__Pssh = null;
+ private byte[] __pbn__Pssh;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue(LicenseType.Zero)]
+ public LicenseType LicenseType
+ {
+ get => __pbn__LicenseType ?? LicenseType.Zero;
+ set => __pbn__LicenseType = value;
+ }
+ public bool ShouldSerializeLicenseType() => __pbn__LicenseType != null;
+ public void ResetLicenseType() => __pbn__LicenseType = null;
+ private LicenseType? __pbn__LicenseType;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] RequestId
+ {
+ get => __pbn__RequestId;
+ set => __pbn__RequestId = value;
+ }
+ public bool ShouldSerializeRequestId() => __pbn__RequestId != null;
+ public void ResetRequestId() => __pbn__RequestId = null;
+ private byte[] __pbn__RequestId;
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class WebM : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public byte[] Header
+ {
+ get => __pbn__Header;
+ set => __pbn__Header = value;
+ }
+ public bool ShouldSerializeHeader() => __pbn__Header != null;
+ public void ResetHeader() => __pbn__Header = null;
+ private byte[] __pbn__Header;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue(LicenseType.Zero)]
+ public LicenseType LicenseType
+ {
+ get => __pbn__LicenseType ?? LicenseType.Zero;
+ set => __pbn__LicenseType = value;
+ }
+ public bool ShouldSerializeLicenseType() => __pbn__LicenseType != null;
+ public void ResetLicenseType() => __pbn__LicenseType = null;
+ private LicenseType? __pbn__LicenseType;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] RequestId
+ {
+ get => __pbn__RequestId;
+ set => __pbn__RequestId = value;
+ }
+ public bool ShouldSerializeRequestId() => __pbn__RequestId != null;
+ public void ResetRequestId() => __pbn__RequestId = null;
+ private byte[] __pbn__RequestId;
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class ExistingLicense : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public LicenseIdentification LicenseId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public uint SecondsSinceStarted
+ {
+ get => __pbn__SecondsSinceStarted.GetValueOrDefault();
+ set => __pbn__SecondsSinceStarted = value;
+ }
+ public bool ShouldSerializeSecondsSinceStarted() => __pbn__SecondsSinceStarted != null;
+ public void ResetSecondsSinceStarted() => __pbn__SecondsSinceStarted = null;
+ private uint? __pbn__SecondsSinceStarted;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public uint SecondsSinceLastPlayed
+ {
+ get => __pbn__SecondsSinceLastPlayed.GetValueOrDefault();
+ set => __pbn__SecondsSinceLastPlayed = value;
+ }
+ public bool ShouldSerializeSecondsSinceLastPlayed() => __pbn__SecondsSinceLastPlayed != null;
+ public void ResetSecondsSinceLastPlayed() => __pbn__SecondsSinceLastPlayed = null;
+ private uint? __pbn__SecondsSinceLastPlayed;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionUsageTableEntry
+ {
+ get => __pbn__SessionUsageTableEntry;
+ set => __pbn__SessionUsageTableEntry = value;
+ }
+ public bool ShouldSerializeSessionUsageTableEntry() => __pbn__SessionUsageTableEntry != null;
+ public void ResetSessionUsageTableEntry() => __pbn__SessionUsageTableEntry = null;
+ private byte[] __pbn__SessionUsageTableEntry;
+
+ }
+
+ }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum RequestType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"NEW")]
+ New = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"RENEWAL")]
+ Renewal = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"RELEASE")]
+ Release = 3,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class ProvisionedDeviceInfo : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public uint SystemId
+ {
+ get => __pbn__SystemId.GetValueOrDefault();
+ set => __pbn__SystemId = value;
+ }
+ public bool ShouldSerializeSystemId() => __pbn__SystemId != null;
+ public void ResetSystemId() => __pbn__SystemId = null;
+ private uint? __pbn__SystemId;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Soc
+ {
+ get => __pbn__Soc ?? "";
+ set => __pbn__Soc = value;
+ }
+ public bool ShouldSerializeSoc() => __pbn__Soc != null;
+ public void ResetSoc() => __pbn__Soc = null;
+ private string __pbn__Soc;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Manufacturer
+ {
+ get => __pbn__Manufacturer ?? "";
+ set => __pbn__Manufacturer = value;
+ }
+ public bool ShouldSerializeManufacturer() => __pbn__Manufacturer != null;
+ public void ResetManufacturer() => __pbn__Manufacturer = null;
+ private string __pbn__Manufacturer;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Model
+ {
+ get => __pbn__Model ?? "";
+ set => __pbn__Model = value;
+ }
+ public bool ShouldSerializeModel() => __pbn__Model != null;
+ public void ResetModel() => __pbn__Model = null;
+ private string __pbn__Model;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string DeviceType
+ {
+ get => __pbn__DeviceType ?? "";
+ set => __pbn__DeviceType = value;
+ }
+ public bool ShouldSerializeDeviceType() => __pbn__DeviceType != null;
+ public void ResetDeviceType() => __pbn__DeviceType = null;
+ private string __pbn__DeviceType;
+
+ [global::ProtoBuf.ProtoMember(6)]
+ public uint ModelYear
+ {
+ get => __pbn__ModelYear.GetValueOrDefault();
+ set => __pbn__ModelYear = value;
+ }
+ public bool ShouldSerializeModelYear() => __pbn__ModelYear != null;
+ public void ResetModelYear() => __pbn__ModelYear = null;
+ private uint? __pbn__ModelYear;
+
+ [global::ProtoBuf.ProtoMember(7)]
+ [global::System.ComponentModel.DefaultValue(WvSecurityLevel.LevelUnspecified)]
+ public WvSecurityLevel SecurityLevel
+ {
+ get => __pbn__SecurityLevel ?? WvSecurityLevel.LevelUnspecified;
+ set => __pbn__SecurityLevel = value;
+ }
+ public bool ShouldSerializeSecurityLevel() => __pbn__SecurityLevel != null;
+ public void ResetSecurityLevel() => __pbn__SecurityLevel = null;
+ private WvSecurityLevel? __pbn__SecurityLevel;
+
+ [global::ProtoBuf.ProtoMember(8)]
+ public uint TestDevice
+ {
+ get => __pbn__TestDevice.GetValueOrDefault();
+ set => __pbn__TestDevice = value;
+ }
+ public bool ShouldSerializeTestDevice() => __pbn__TestDevice != null;
+ public void ResetTestDevice() => __pbn__TestDevice = null;
+ private uint? __pbn__TestDevice;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum WvSecurityLevel
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"LEVEL_UNSPECIFIED")]
+ LevelUnspecified = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"LEVEL_1")]
+ Level1 = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"LEVEL_2")]
+ Level2 = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"LEVEL_3")]
+ Level3 = 3,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class RemoteAttestation : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ public EncryptedClientIdentification Certificate { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Salt
+ {
+ get => __pbn__Salt ?? "";
+ set => __pbn__Salt = value;
+ }
+ public bool ShouldSerializeSalt() => __pbn__Salt != null;
+ public void ResetSalt() => __pbn__Salt = null;
+ private string __pbn__Salt;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Signature
+ {
+ get => __pbn__Signature ?? "";
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private string __pbn__Signature;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class ProvisioningOptions : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(CertificateType.WidevineDrm)]
+ public CertificateType certificate_type
+ {
+ get => __pbn__certificate_type ?? CertificateType.WidevineDrm;
+ set => __pbn__certificate_type = value;
+ }
+ public bool ShouldSerializecertificate_type() => __pbn__certificate_type != null;
+ public void Resetcertificate_type() => __pbn__certificate_type = null;
+ private CertificateType? __pbn__certificate_type;
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"certificate_authority")]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string CertificateAuthority
+ {
+ get => __pbn__CertificateAuthority ?? "";
+ set => __pbn__CertificateAuthority = value;
+ }
+ public bool ShouldSerializeCertificateAuthority() => __pbn__CertificateAuthority != null;
+ public void ResetCertificateAuthority() => __pbn__CertificateAuthority = null;
+ private string __pbn__CertificateAuthority;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum CertificateType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"WIDEVINE_DRM")]
+ WidevineDrm = 0,
+ X509 = 1,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class ProvisioningRequest : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"client_id")]
+ public ClientIdentification ClientId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(5, Name = @"encrypted_client_id")]
+ public EncryptedClientIdentification EncryptedClientId { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"nonce")]
+ public byte[] Nonce
+ {
+ get => __pbn__Nonce;
+ set => __pbn__Nonce = value;
+ }
+ public bool ShouldSerializeNonce() => __pbn__Nonce != null;
+ public void ResetNonce() => __pbn__Nonce = null;
+ private byte[] __pbn__Nonce;
+
+ [global::ProtoBuf.ProtoMember(3, Name = @"options")]
+ public ProvisioningOptions Options { get; set; }
+
+ [global::ProtoBuf.ProtoMember(4, Name = @"stable_id")]
+ public byte[] StableId
+ {
+ get => __pbn__StableId;
+ set => __pbn__StableId = value;
+ }
+ public bool ShouldSerializeStableId() => __pbn__StableId != null;
+ public void ResetStableId() => __pbn__StableId = null;
+ private byte[] __pbn__StableId;
+
+ [global::ProtoBuf.ProtoMember(6, Name = @"provider_id")]
+ public byte[] ProviderId
+ {
+ get => __pbn__ProviderId;
+ set => __pbn__ProviderId = value;
+ }
+ public bool ShouldSerializeProviderId() => __pbn__ProviderId != null;
+ public void ResetProviderId() => __pbn__ProviderId = null;
+ private byte[] __pbn__ProviderId;
+
+ [global::ProtoBuf.ProtoMember(7, Name = @"spoid")]
+ public byte[] Spoid
+ {
+ get => __pbn__Spoid;
+ set => __pbn__Spoid = value;
+ }
+ public bool ShouldSerializeSpoid() => __pbn__Spoid != null;
+ public void ResetSpoid() => __pbn__Spoid = null;
+ private byte[] __pbn__Spoid;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class ProvisioningResponse : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"device_rsa_key")]
+ public byte[] DeviceRsaKey
+ {
+ get => __pbn__DeviceRsaKey;
+ set => __pbn__DeviceRsaKey = value;
+ }
+ public bool ShouldSerializeDeviceRsaKey() => __pbn__DeviceRsaKey != null;
+ public void ResetDeviceRsaKey() => __pbn__DeviceRsaKey = null;
+ private byte[] __pbn__DeviceRsaKey;
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"device_rsa_key_iv")]
+ public byte[] DeviceRsaKeyIv
+ {
+ get => __pbn__DeviceRsaKeyIv;
+ set => __pbn__DeviceRsaKeyIv = value;
+ }
+ public bool ShouldSerializeDeviceRsaKeyIv() => __pbn__DeviceRsaKeyIv != null;
+ public void ResetDeviceRsaKeyIv() => __pbn__DeviceRsaKeyIv = null;
+ private byte[] __pbn__DeviceRsaKeyIv;
+
+ [global::ProtoBuf.ProtoMember(3, Name = @"device_certificate")]
+ public SignedDeviceCertificate DeviceCertificate { get; set; }
+
+ [global::ProtoBuf.ProtoMember(4, Name = @"nonce")]
+ public byte[] Nonce
+ {
+ get => __pbn__Nonce;
+ set => __pbn__Nonce = value;
+ }
+ public bool ShouldSerializeNonce() => __pbn__Nonce != null;
+ public void ResetNonce() => __pbn__Nonce = null;
+ private byte[] __pbn__Nonce;
+
+ [global::ProtoBuf.ProtoMember(5, Name = @"wrapping_key")]
+ public byte[] WrappingKey
+ {
+ get => __pbn__WrappingKey;
+ set => __pbn__WrappingKey = value;
+ }
+ public bool ShouldSerializeWrappingKey() => __pbn__WrappingKey != null;
+ public void ResetWrappingKey() => __pbn__WrappingKey = null;
+ private byte[] __pbn__WrappingKey;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedProvisioningMessage : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"message")]
+ public ProvisioningResponse Message { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"signature")]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ [global::System.ComponentModel.DefaultValue(ProtocolVersion.Version2)]
+ public ProtocolVersion protocol_version
+ {
+ get => __pbn__protocol_version ?? ProtocolVersion.Version2;
+ set => __pbn__protocol_version = value;
+ }
+ public bool ShouldSerializeprotocol_version() => __pbn__protocol_version != null;
+ public void Resetprotocol_version() => __pbn__protocol_version = null;
+ private ProtocolVersion? __pbn__protocol_version;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum ProtocolVersion
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"VERSION_2")]
+ Version2 = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"VERSION_3")]
+ Version3 = 3,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class DeviceCertificateHack0 : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"message")]
+ public DeviceCertificateHack1 Message { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"signature")]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class DeviceCertificateHack1 : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(3, Name = @"message")]
+ public DeviceCertificateHack2 Message { get; set; }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class DeviceCertificateHack2 : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"device_certificate")]
+ public SignedDeviceCertificate DeviceCertificate { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] extraData
+ {
+ get => __pbn__extraData;
+ set => __pbn__extraData = value;
+ }
+ public bool ShouldSerializeextraData() => __pbn__extraData != null;
+ public void ResetextraData() => __pbn__extraData = null;
+ private byte[] __pbn__extraData;
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedDeviceCertificate : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"_DeviceCertificate")]
+ public DeviceCertificate DeviceCertificate { get; set; }
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public SignedDeviceCertificate Signer { get; set; }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedMessage : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(MessageType.LicenseRequest)]
+ public MessageType Type
+ {
+ get => __pbn__Type ?? MessageType.LicenseRequest;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private MessageType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public byte[] Msg
+ {
+ get => __pbn__Msg;
+ set => __pbn__Msg = value;
+ }
+ public bool ShouldSerializeMsg() => __pbn__Msg != null;
+ public void ResetMsg() => __pbn__Msg = null;
+ private byte[] __pbn__Msg;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionKey
+ {
+ get => __pbn__SessionKey;
+ set => __pbn__SessionKey = value;
+ }
+ public bool ShouldSerializeSessionKey() => __pbn__SessionKey != null;
+ public void ResetSessionKey() => __pbn__SessionKey = null;
+ private byte[] __pbn__SessionKey;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public RemoteAttestation RemoteAttestation { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum MessageType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE_REQUEST")]
+ LicenseRequest = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE")]
+ License = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"ERROR_RESPONSE")]
+ ErrorResponse = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE_REQUEST")]
+ ServiceCertificateRequest = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE")]
+ ServiceCertificate = 5,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class WidevineCencHeader : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(Algorithm.Unencrypted)]
+ public Algorithm algorithm
+ {
+ get => __pbn__algorithm ?? Algorithm.Unencrypted;
+ set => __pbn__algorithm = value;
+ }
+ public bool ShouldSerializealgorithm() => __pbn__algorithm != null;
+ public void Resetalgorithm() => __pbn__algorithm = null;
+ private Algorithm? __pbn__algorithm;
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"key_id")]
+ public global::System.Collections.Generic.List KeyIds { get; } = new global::System.Collections.Generic.List();
+
+ [global::ProtoBuf.ProtoMember(3, Name = @"provider")]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Provider
+ {
+ get => __pbn__Provider ?? "";
+ set => __pbn__Provider = value;
+ }
+ public bool ShouldSerializeProvider() => __pbn__Provider != null;
+ public void ResetProvider() => __pbn__Provider = null;
+ private string __pbn__Provider;
+
+ [global::ProtoBuf.ProtoMember(4, Name = @"content_id")]
+ public byte[] ContentId
+ {
+ get => __pbn__ContentId;
+ set => __pbn__ContentId = value;
+ }
+ public bool ShouldSerializeContentId() => __pbn__ContentId != null;
+ public void ResetContentId() => __pbn__ContentId = null;
+ private byte[] __pbn__ContentId;
+
+ [global::ProtoBuf.ProtoMember(5, Name = @"track_type_deprecated")]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string TrackTypeDeprecated
+ {
+ get => __pbn__TrackTypeDeprecated ?? "";
+ set => __pbn__TrackTypeDeprecated = value;
+ }
+ public bool ShouldSerializeTrackTypeDeprecated() => __pbn__TrackTypeDeprecated != null;
+ public void ResetTrackTypeDeprecated() => __pbn__TrackTypeDeprecated = null;
+ private string __pbn__TrackTypeDeprecated;
+
+ [global::ProtoBuf.ProtoMember(6, Name = @"policy")]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Policy
+ {
+ get => __pbn__Policy ?? "";
+ set => __pbn__Policy = value;
+ }
+ public bool ShouldSerializePolicy() => __pbn__Policy != null;
+ public void ResetPolicy() => __pbn__Policy = null;
+ private string __pbn__Policy;
+
+ [global::ProtoBuf.ProtoMember(7, Name = @"crypto_period_index")]
+ public uint CryptoPeriodIndex
+ {
+ get => __pbn__CryptoPeriodIndex.GetValueOrDefault();
+ set => __pbn__CryptoPeriodIndex = value;
+ }
+ public bool ShouldSerializeCryptoPeriodIndex() => __pbn__CryptoPeriodIndex != null;
+ public void ResetCryptoPeriodIndex() => __pbn__CryptoPeriodIndex = null;
+ private uint? __pbn__CryptoPeriodIndex;
+
+ [global::ProtoBuf.ProtoMember(8, Name = @"grouped_license")]
+ public byte[] GroupedLicense
+ {
+ get => __pbn__GroupedLicense;
+ set => __pbn__GroupedLicense = value;
+ }
+ public bool ShouldSerializeGroupedLicense() => __pbn__GroupedLicense != null;
+ public void ResetGroupedLicense() => __pbn__GroupedLicense = null;
+ private byte[] __pbn__GroupedLicense;
+
+ [global::ProtoBuf.ProtoMember(9, Name = @"protection_scheme")]
+ public uint ProtectionScheme
+ {
+ get => __pbn__ProtectionScheme.GetValueOrDefault();
+ set => __pbn__ProtectionScheme = value;
+ }
+ public bool ShouldSerializeProtectionScheme() => __pbn__ProtectionScheme != null;
+ public void ResetProtectionScheme() => __pbn__ProtectionScheme = null;
+ private uint? __pbn__ProtectionScheme;
+
+ [global::ProtoBuf.ProtoMember(10, Name = @"crypto_period_seconds")]
+ public uint CryptoPeriodSeconds
+ {
+ get => __pbn__CryptoPeriodSeconds.GetValueOrDefault();
+ set => __pbn__CryptoPeriodSeconds = value;
+ }
+ public bool ShouldSerializeCryptoPeriodSeconds() => __pbn__CryptoPeriodSeconds != null;
+ public void ResetCryptoPeriodSeconds() => __pbn__CryptoPeriodSeconds = null;
+ private uint? __pbn__CryptoPeriodSeconds;
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum Algorithm
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"UNENCRYPTED")]
+ Unencrypted = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"AESCTR")]
+ Aesctr = 1,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedLicenseRequest : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(MessageType.LicenseRequest)]
+ public MessageType Type
+ {
+ get => __pbn__Type ?? MessageType.LicenseRequest;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private MessageType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public LicenseRequest Msg { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionKey
+ {
+ get => __pbn__SessionKey;
+ set => __pbn__SessionKey = value;
+ }
+ public bool ShouldSerializeSessionKey() => __pbn__SessionKey != null;
+ public void ResetSessionKey() => __pbn__SessionKey = null;
+ private byte[] __pbn__SessionKey;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public RemoteAttestation RemoteAttestation { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum MessageType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE_REQUEST")]
+ LicenseRequest = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE")]
+ License = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"ERROR_RESPONSE")]
+ ErrorResponse = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE_REQUEST")]
+ ServiceCertificateRequest = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE")]
+ ServiceCertificate = 5,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedLicenseRequestRaw : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(MessageType.LicenseRequest)]
+ public MessageType Type
+ {
+ get => __pbn__Type ?? MessageType.LicenseRequest;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private MessageType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public LicenseRequestRaw Msg { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionKey
+ {
+ get => __pbn__SessionKey;
+ set => __pbn__SessionKey = value;
+ }
+ public bool ShouldSerializeSessionKey() => __pbn__SessionKey != null;
+ public void ResetSessionKey() => __pbn__SessionKey = null;
+ private byte[] __pbn__SessionKey;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public RemoteAttestation RemoteAttestation { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum MessageType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE_REQUEST")]
+ LicenseRequest = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE")]
+ License = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"ERROR_RESPONSE")]
+ ErrorResponse = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE_REQUEST")]
+ ServiceCertificateRequest = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE")]
+ ServiceCertificate = 5,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedLicense : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(MessageType.LicenseRequest)]
+ public MessageType Type
+ {
+ get => __pbn__Type ?? MessageType.LicenseRequest;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private MessageType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public License Msg { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionKey
+ {
+ get => __pbn__SessionKey;
+ set => __pbn__SessionKey = value;
+ }
+ public bool ShouldSerializeSessionKey() => __pbn__SessionKey != null;
+ public void ResetSessionKey() => __pbn__SessionKey = null;
+ private byte[] __pbn__SessionKey;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public RemoteAttestation RemoteAttestation { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum MessageType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE_REQUEST")]
+ LicenseRequest = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE")]
+ License = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"ERROR_RESPONSE")]
+ ErrorResponse = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE_REQUEST")]
+ ServiceCertificateRequest = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE")]
+ ServiceCertificate = 5,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class SignedServiceCertificate : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1)]
+ [global::System.ComponentModel.DefaultValue(MessageType.LicenseRequest)]
+ public MessageType Type
+ {
+ get => __pbn__Type ?? MessageType.LicenseRequest;
+ set => __pbn__Type = value;
+ }
+ public bool ShouldSerializeType() => __pbn__Type != null;
+ public void ResetType() => __pbn__Type = null;
+ private MessageType? __pbn__Type;
+
+ [global::ProtoBuf.ProtoMember(2)]
+ public SignedDeviceCertificate Msg { get; set; }
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] Signature
+ {
+ get => __pbn__Signature;
+ set => __pbn__Signature = value;
+ }
+ public bool ShouldSerializeSignature() => __pbn__Signature != null;
+ public void ResetSignature() => __pbn__Signature = null;
+ private byte[] __pbn__Signature;
+
+ [global::ProtoBuf.ProtoMember(4)]
+ public byte[] SessionKey
+ {
+ get => __pbn__SessionKey;
+ set => __pbn__SessionKey = value;
+ }
+ public bool ShouldSerializeSessionKey() => __pbn__SessionKey != null;
+ public void ResetSessionKey() => __pbn__SessionKey = null;
+ private byte[] __pbn__SessionKey;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public RemoteAttestation RemoteAttestation { get; set; }
+
+ [global::ProtoBuf.ProtoContract()]
+ public enum MessageType
+ {
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE_REQUEST")]
+ LicenseRequest = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"LICENSE")]
+ License = 2,
+ [global::ProtoBuf.ProtoEnum(Name = @"ERROR_RESPONSE")]
+ ErrorResponse = 3,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE_REQUEST")]
+ ServiceCertificateRequest = 4,
+ [global::ProtoBuf.ProtoEnum(Name = @"SERVICE_CERTIFICATE")]
+ ServiceCertificate = 5,
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public partial class FileHashes : global::ProtoBuf.IExtensible
+{
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"signer")]
+ public byte[] Signer
+ {
+ get => __pbn__Signer;
+ set => __pbn__Signer = value;
+ }
+ public bool ShouldSerializeSigner() => __pbn__Signer != null;
+ public void ResetSigner() => __pbn__Signer = null;
+ private byte[] __pbn__Signer;
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"signatures")]
+ public global::System.Collections.Generic.List Signatures { get; } = new global::System.Collections.Generic.List();
+
+ [global::ProtoBuf.ProtoContract()]
+ public partial class Signature : global::ProtoBuf.IExtensible
+ {
+ private global::ProtoBuf.IExtension __pbn__extensionData;
+ global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
+ => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);
+
+ [global::ProtoBuf.ProtoMember(1, Name = @"filename")]
+ [global::System.ComponentModel.DefaultValue("")]
+ public string Filename
+ {
+ get => __pbn__Filename ?? "";
+ set => __pbn__Filename = value;
+ }
+ public bool ShouldSerializeFilename() => __pbn__Filename != null;
+ public void ResetFilename() => __pbn__Filename = null;
+ private string __pbn__Filename;
+
+ [global::ProtoBuf.ProtoMember(2, Name = @"test_signing")]
+ public bool TestSigning
+ {
+ get => __pbn__TestSigning.GetValueOrDefault();
+ set => __pbn__TestSigning = value;
+ }
+ public bool ShouldSerializeTestSigning() => __pbn__TestSigning != null;
+ public void ResetTestSigning() => __pbn__TestSigning = null;
+ private bool? __pbn__TestSigning;
+
+ [global::ProtoBuf.ProtoMember(3)]
+ public byte[] SHA512Hash
+ {
+ get => __pbn__SHA512Hash;
+ set => __pbn__SHA512Hash = value;
+ }
+ public bool ShouldSerializeSHA512Hash() => __pbn__SHA512Hash != null;
+ public void ResetSHA512Hash() => __pbn__SHA512Hash = null;
+ private byte[] __pbn__SHA512Hash;
+
+ [global::ProtoBuf.ProtoMember(4, Name = @"main_exe")]
+ public bool MainExe
+ {
+ get => __pbn__MainExe.GetValueOrDefault();
+ set => __pbn__MainExe = value;
+ }
+ public bool ShouldSerializeMainExe() => __pbn__MainExe != null;
+ public void ResetMainExe() => __pbn__MainExe = null;
+ private bool? __pbn__MainExe;
+
+ [global::ProtoBuf.ProtoMember(5)]
+ public byte[] signature
+ {
+ get => __pbn__signature;
+ set => __pbn__signature = value;
+ }
+ public bool ShouldSerializesignature() => __pbn__signature != null;
+ public void Resetsignature() => __pbn__signature = null;
+ private byte[] __pbn__signature;
+
+ }
+
+}
+
+[global::ProtoBuf.ProtoContract()]
+public enum LicenseType
+{
+ [global::ProtoBuf.ProtoEnum(Name = @"ZERO")]
+ Zero = 0,
+ [global::ProtoBuf.ProtoEnum(Name = @"DEFAULT")]
+ Default = 1,
+ [global::ProtoBuf.ProtoEnum(Name = @"OFFLINE")]
+ Offline = 2,
+}
+
+[global::ProtoBuf.ProtoContract()]
+public enum ProtocolVersion
+{
+ [global::ProtoBuf.ProtoEnum(Name = @"CURRENT")]
+ Current = 21,
+}
+
+#pragma warning restore CS0612, CS0618, CS1591, CS3021, IDE0079, IDE1006, RCS1036, RCS1057, RCS1085, RCS1192
+#endregion
diff --git a/Utils/Enums/EnumCollection.cs b/Utils/Enums/EnumCollection.cs
new file mode 100644
index 0000000..a2671f3
--- /dev/null
+++ b/Utils/Enums/EnumCollection.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Runtime.Serialization;
+using CRD.Utils.JsonConv;
+using Newtonsoft.Json;
+
+namespace CRD.Utils;
+
+[DataContract]
+[JsonConverter(typeof(LocaleConverter))]
+public enum Locale{
+ [EnumMember(Value = "")] DefaulT,
+ [EnumMember(Value = "un")] Unknown,
+ [EnumMember(Value = "en-US")] EnUs,
+ [EnumMember(Value = "es-LA")] EsLa,
+ [EnumMember(Value = "es-419")] Es419,
+ [EnumMember(Value = "es-ES")] EsEs,
+ [EnumMember(Value = "pt-BR")] PtBr,
+ [EnumMember(Value = "fr-FR")] FrFr,
+ [EnumMember(Value = "de-DE")] DeDe,
+ [EnumMember(Value = "ar-ME")] ArMe,
+ [EnumMember(Value = "ar-SA")] ArSa,
+ [EnumMember(Value = "it-IT")] ItIt,
+ [EnumMember(Value = "ru-RU")] RuRu,
+ [EnumMember(Value = "tr-TR")] TrTr,
+ [EnumMember(Value = "hi-IN")] HiIn,
+ [EnumMember(Value = "zh-CN")] ZhCn,
+ [EnumMember(Value = "ko-KR")] KoKr,
+ [EnumMember(Value = "ja-JP")] JaJp,
+ [EnumMember(Value = "id-ID")] IdId,
+}
+
+public static class EnumExtensions{
+ public static string GetEnumMemberValue(this Enum value){
+ var type = value.GetType();
+ var name = Enum.GetName(type, value);
+ if (name != null){
+ var field = type.GetField(name);
+ if (field != null){
+ var attr = Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)) as EnumMemberAttribute;
+ if (attr != null){
+ return attr.Value ?? string.Empty;
+ }
+ }
+ }
+
+ return string.Empty;
+ }
+}
+
+[DataContract]
+public enum ChannelId{
+ [EnumMember(Value = "crunchyroll")] Crunchyroll,
+}
+
+[DataContract]
+public enum ImageType{
+ [EnumMember(Value = "poster_tall")] PosterTall,
+
+ [EnumMember(Value = "poster_wide")] PosterWide,
+
+ [EnumMember(Value = "promo_image")] PromoImage,
+
+ [EnumMember(Value = "thumbnail")] Thumbnail,
+}
+
+[DataContract]
+public enum MaturityRating{
+ [EnumMember(Value = "TV-14")] Tv14,
+}
+
+[DataContract]
+public enum MediaType{
+ [EnumMember(Value = "episode")] Episode,
+}
+
+[DataContract]
+public enum DownloadMediaType{
+ [EnumMember(Value = "Video")] Video,
+ [EnumMember(Value = "Audio")] Audio,
+ [EnumMember(Value = "Chapters")] Chapters,
+ [EnumMember(Value = "Subtitle")] Subtitle,
+}
\ No newline at end of file
diff --git a/Utils/Files/CfgManager.cs b/Utils/Files/CfgManager.cs
new file mode 100644
index 0000000..93a7644
--- /dev/null
+++ b/Utils/Files/CfgManager.cs
@@ -0,0 +1,182 @@
+using System;
+using System.IO;
+using CRD.Downloader;
+using CRD.Utils.Structs;
+using Newtonsoft.Json;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace CRD.Utils;
+
+public class CfgManager{
+ private static string WorkingDirectory = Directory.GetCurrentDirectory();
+
+ public static readonly string PathCrToken = WorkingDirectory + "/config/cr_token.yml";
+ public static readonly string PathCrDownloadOptions = WorkingDirectory + "/config/settings.yml";
+ public static readonly string PathCrHistory = WorkingDirectory + "/config/history.json";
+
+ public static readonly string PathFFMPEG = WorkingDirectory + "/lib/ffmpeg.exe";
+ public static readonly string PathMKVMERGE = WorkingDirectory + "/lib/mkvmerge.exe";
+ public static readonly string PathMP4Decrypt = WorkingDirectory + "/lib/mp4decrypt.exe";
+
+ public static readonly string PathWIDEVINE_DIR = WorkingDirectory + "/widevine/";
+
+ public static readonly string PathVIDEOS_DIR = WorkingDirectory + "/video/";
+ public static readonly string PathFONTS_DIR = WorkingDirectory + "/video/";
+
+
+ public static void WriteJsonResponseToYamlFile(string jsonResponse, string filePath){
+ // Convert JSON to an object
+ var deserializer = new DeserializerBuilder()
+ .WithNamingConvention(UnderscoredNamingConvention.Instance) // Adjust this as needed
+ .Build();
+ var jsonObject = deserializer.Deserialize