Crunchy-Downloader/CRD/Downloader/Crunchyroll.cs

1617 lines
80 KiB
C#
Raw Normal View History

2024-05-04 15:35:32 +00:00
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<CrunchyEpMeta> Queue = new RefreshableObservableCollection<CrunchyEpMeta>();
public ObservableCollection<DownloadItemModel> DownloadItemModels = new ObservableCollection<DownloadItemModel>();
public int ActiveDownloads;
public bool AutoDownload = false;
#endregion
#region Calendar Variables
private Dictionary<string, CalendarWeek> calendar = new();
private Dictionary<string, string> calendarLanguage = new();
#endregion
#region History Variables
public ObservableCollection<HistorySeries> 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<CrToken>(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<string>{ "de-DE" };
CrunOptions.Skipmux = false;
CrunOptions.MkvmergeOptions = new List<string>{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" };
CrunOptions.DefaultAudio = Languages.FindLang("ja-JP");
CrunOptions.DefaultSub = Languages.FindLang("de-DE");
CrunOptions.CcTag = "cc";
CrunOptions.FsRetryTime = 5;
CrunOptions.Numbers = 2;
CrunOptions.Timeout = 15000;
CrunOptions.DubLang = new List<string>(){ "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<ObservableCollection<HistorySeries>>(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<CalendarWeek> 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<CalendarDay>();
// 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<CalendarEpisode>();
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 Task AddEpisodeToQue(string epId, string locale, List<string> dubLang){
2024-05-04 15:35:32 +00:00
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<bool> 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<DownloadedMedia> data, CrunchyMuxOptions options, string filename){
var hasAudioStreams = false;
var muxToMp3 = false;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
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<SubtitleFonts>();
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<MergerInput>(),
SkipSubMux = options.SkipSubMux,
OnlyAudio = hasAudioStreams ? data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList() : new List<MergerInput>(),
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc, Signs = a.Signs }).ToList(),
Simul = false,
KeepAllVideos = options.KeepAllVideos,
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined
VideoAndAudio = hasAudioStreams ? new List<MergerInput>() : data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle,
Options = new MuxOptions(){
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<DownloadResponse> 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<DownloadedMedia>(),
Error = true,
FileName = "./unknown"
};
}
if (Profile.Username == "???"){
MainWindow.ShowError("User Account not recognized - are you signed in?");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
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<DownloadedMedia>(),
Error = true,
FileName = "./unknown"
};
}
string mediaName = $"{data.SeasonTitle} - {data.EpisodeNumber} - {data.EpisodeTitle}";
string fileName = "";
var variables = new List<Variable>();
List<DownloadedMedia> files = new List<DownloadedMedia>();
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<string> compiledChapters = new List<string>();
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<DownloadedMedia>(),
Error = true,
FileName = "./unknown"
};
}
var pbData = fetchPlaybackData.pbData;
#region NonDrmRequest
await FetchNoDrmPlaybackData(mediaGuid, pbData);
#endregion
List<string> hsLangs = new List<string>();
var pbStreams = pbData.Data?[0];
var streams = new List<StreamDetailsPop>();
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<DownloadedMedia>(),
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){
2024-05-05 16:03:07 +00:00
2024-05-04 15:35:32 +00:00
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<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
2024-05-05 16:03:07 +00:00
options.StreamServer = options.StreamServer > streamServers.Count ? 1 : options.StreamServer;
2024-05-04 15:35:32 +00:00
if (streamServers.Count == 0){
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown"
};
}
2024-05-05 16:03:07 +00:00
if (options.StreamServer == 0){
options.StreamServer = 1;
2024-05-04 15:35:32 +00:00
}
2024-05-05 16:03:07 +00:00
string selectedServer = streamServers[options.StreamServer - 1];
2024-05-04 15:35:32 +00:00
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<DownloadedMedia>(),
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<DrmAuthData>(decRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new DrmAuthData();
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "dt-custom-data", authData.CustomData ?? string.Empty },{ "x-dt-auth-token", authData.Token ?? string.Empty } };
var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, "https://lic.drmtoday.com/license-proxy-widevine/cenc/", authDataDict);
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<string>();
// 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<string>{ "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<DownloadedMedia> files){
if (pbData.Meta != null && pbData.Meta.Subtitles != null && pbData.Meta.Subtitles.Count > 0){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles.Values.ToList();
List<SubtitleInfo> capsData = pbData.Meta.ClosedCaptions?.Values.ToList() ?? new List<SubtitleInfo>();
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<string>();
// 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<string>();
// 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<dynamic>().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<string>();
// 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<dynamic>().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<CrunchyNoDrmStream>(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<Dictionary<string, Dictionary<string, StreamDetails>>>() };
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<PlaybackDataAndroid>(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<PlaybackData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
} 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<PlaybackData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
} else{
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
playbackRequest = HttpClientReq.CreateRequestMessage($"{Api.ApiBeta}{epMeta.Playback}", HttpMethod.Get, true, true, null);
playbackRequestResponse = await HttpClientReq.Instance.SendHttpRequest(playbackRequest);
if (playbackRequestResponse.IsOk){
temppbData = Helpers.Deserialize<PlaybackData>(playbackRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ??
new PlaybackData{ Total = 0, Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>() };
} else{
Console.WriteLine("'Fallback Request Stream URLs FAILED!'");
ok = playbackRequestResponse.IsOk;
}
}
}
return (IsOk: ok, pbData: temppbData);
}
private async Task ParseChapters(string currentMediaId, List<string> 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<DateTime?>();
chapterData.mediaId = jObject["mediaId"]?.ToObject<string>();
chapterData.Chapters = new List<CrunchyChapter>();
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<CrunchyChapter>();
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<CrunchyOldChapter>(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");
}
}
}
}