mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-07-27 19:32:10 +00:00
Compare commits
No commits in common. "master" and "v1.6.10-pre.2" have entirely different histories.
master
...
v1.6.10-pr
43 changed files with 918 additions and 2690 deletions
|
|
@ -7,7 +7,6 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Downloader.Crunchyroll.Utils;
|
||||
using CRD.Utils;
|
||||
|
|
@ -26,10 +25,6 @@ public class CalendarManager{
|
|||
#region Calendar Variables
|
||||
|
||||
private Dictionary<string, CalendarWeek> calendar = new();
|
||||
private DateTime? anilistUpcomingLoadedDate;
|
||||
private static readonly Regex GenericSeasonLabelRegex = new(
|
||||
@"^(?<word>\p{L}+(?:[\p{L}\p{Mn}'\.\- ]*\p{L})?)\s+(?<n>\d+)$",
|
||||
RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
|
||||
private Dictionary<string, string> calendarLanguage = new(){
|
||||
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
|
||||
|
|
@ -72,7 +67,6 @@ public class CalendarManager{
|
|||
|
||||
public async Task<CalendarWeek> GetCalendarForDate(string weeksMondayDate, bool forceUpdate){
|
||||
if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){
|
||||
RefreshHistoryStatuses(forDate);
|
||||
return forDate;
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +197,6 @@ public class CalendarManager{
|
|||
}
|
||||
|
||||
calendar[weeksMondayDate] = week;
|
||||
RefreshHistoryStatuses(week);
|
||||
|
||||
|
||||
return week;
|
||||
|
|
@ -211,15 +204,15 @@ public class CalendarManager{
|
|||
|
||||
|
||||
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
|
||||
var crunInstance = CrunchyrollManager.Instance;
|
||||
var crunOptions = crunInstance.CrunOptions;
|
||||
var calendarKey = "C" + calTargetDate.ToString("yyyy-MM-dd");
|
||||
|
||||
if (!forceUpdate && calendar.TryGetValue(calendarKey, out var forDate)){
|
||||
RefreshHistoryStatuses(forDate);
|
||||
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
|
||||
return forDate;
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
await LoadAnilistUpcoming();
|
||||
}
|
||||
|
||||
|
||||
CalendarWeek week = new CalendarWeek();
|
||||
week.CalendarDays = new List<CalendarDay>();
|
||||
|
||||
|
|
@ -240,34 +233,20 @@ public class CalendarManager{
|
|||
var firstDayOfWeek = week.CalendarDays.First().DateTime;
|
||||
week.FirstDayOfWeek = firstDayOfWeek;
|
||||
|
||||
var calendarDaysByDate = week.CalendarDays.ToDictionary(day => day.DateTime.Date);
|
||||
var episodeMergeIndexByDate = week.CalendarDays.ToDictionary(
|
||||
day => day.DateTime.Date,
|
||||
_ => new Dictionary<(string? CrSeriesID, Locale? AudioLocale), CalendarEpisode>());
|
||||
|
||||
Task anilistUpcomingTask = crunOptions.CalendarShowUpcomingEpisodes
|
||||
? LoadAnilistUpcoming()
|
||||
: Task.CompletedTask;
|
||||
Task<CrBrowseEpisodeBase?> newEpisodesTask = crunInstance.CrEpisode.GetNewEpisodes(crunOptions.HistoryLang, 2000, firstDayOfWeek, true);
|
||||
|
||||
await Task.WhenAll(anilistUpcomingTask, newEpisodesTask);
|
||||
|
||||
var newEpisodesBase = await newEpisodesTask;
|
||||
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 2000, null, true);
|
||||
|
||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||
var newEpisodes = newEpisodesBase.Data ?? [];
|
||||
|
||||
if (crunOptions.UpdateHistoryFromCalendar){
|
||||
QueueHistoryUpdateFromCalendar(newEpisodes);
|
||||
if (CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar){
|
||||
try{
|
||||
await CrunchyrollManager.Instance.History.UpdateWithEpisode(newEpisodes);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
} catch (Exception e){
|
||||
Console.Error.WriteLine("Failed to update History from calendar");
|
||||
}
|
||||
}
|
||||
|
||||
DateTime now = DateTime.Now;
|
||||
DateTime nowDate = now.Date;
|
||||
DateTime oneYearFromNow = now.AddYears(1);
|
||||
var dubFilter = crunOptions.CalendarDubFilter;
|
||||
bool hasDubFilter = !string.IsNullOrEmpty(dubFilter) && dubFilter != "none";
|
||||
string? historyLang = crunOptions.HistoryLang;
|
||||
|
||||
|
||||
//EpisodeAirDate
|
||||
foreach (var crBrowseEpisode in newEpisodes){
|
||||
bool filtered = false;
|
||||
|
|
@ -279,6 +258,9 @@ public class CalendarManager{
|
|||
? crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.ToLocalTime()
|
||||
: crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate;
|
||||
|
||||
DateTime now = DateTime.Now;
|
||||
DateTime oneYearFromNow = now.AddYears(1);
|
||||
|
||||
DateTime targetDate;
|
||||
|
||||
|
||||
|
|
@ -297,52 +279,53 @@ public class CalendarManager{
|
|||
}
|
||||
|
||||
|
||||
if (crunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
|
||||
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
|
||||
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Audio)")) &&
|
||||
(!hasDubFilter || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){
|
||||
(string.IsNullOrEmpty(dubFilter) || dubFilter == "none" || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){
|
||||
//|| crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp
|
||||
filtered = true;
|
||||
}
|
||||
|
||||
|
||||
if (hasDubFilter){
|
||||
if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){
|
||||
if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){
|
||||
filtered = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (calendarDaysByDate.TryGetValue(targetDate.Date, out var calendarDay)){
|
||||
var calendarDay = (from day in week.CalendarDays
|
||||
where day.DateTime != DateTime.MinValue && day.DateTime.Date == targetDate.Date
|
||||
select day).FirstOrDefault();
|
||||
|
||||
if (calendarDay != null){
|
||||
CalendarEpisode calEpisode = new CalendarEpisode();
|
||||
|
||||
string? seasonTitle = string.IsNullOrEmpty(crBrowseEpisode.EpisodeMetadata.SeasonTitle)
|
||||
? crBrowseEpisode.EpisodeMetadata.SeriesTitle
|
||||
: LooksLikeGenericSeasonLabel(crBrowseEpisode.EpisodeMetadata.SeasonTitle)
|
||||
: LooksLikeGenericSeasonLabel(crBrowseEpisode.EpisodeMetadata.SeasonTitle, crBrowseEpisode.EpisodeMetadata.SeasonNumber)
|
||||
? $"{crBrowseEpisode.EpisodeMetadata.SeriesTitle} {crBrowseEpisode.EpisodeMetadata.SeasonTitle}"
|
||||
: crBrowseEpisode.EpisodeMetadata.SeasonTitle;
|
||||
|
||||
calEpisode.DateTime = targetDate;
|
||||
calEpisode.HasPassed = now > targetDate;
|
||||
calEpisode.HasPassed = DateTime.Now > targetDate;
|
||||
calEpisode.EpisodeName = crBrowseEpisode.Title;
|
||||
calEpisode.SeriesUrl = $"https://www.crunchyroll.com/{historyLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
|
||||
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/{historyLang}/watch/{crBrowseEpisode.Id}/";
|
||||
calEpisode.SeriesUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
|
||||
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/watch/{crBrowseEpisode.Id}/";
|
||||
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail?.FirstOrDefault()?.FirstOrDefault()?.Source ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
|
||||
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
|
||||
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
|
||||
calEpisode.SeasonName = seasonTitle;
|
||||
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
|
||||
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
|
||||
calEpisode.CrSeasonID = crBrowseEpisode.EpisodeMetadata.SeasonId;
|
||||
calEpisode.CrEpisodeID = crBrowseEpisode.Id;
|
||||
calEpisode.FilteredOut = filtered;
|
||||
calEpisode.AudioLocale = crBrowseEpisode.EpisodeMetadata.AudioLocale;
|
||||
calEpisode.Versions = crBrowseEpisode.EpisodeMetadata.versions;
|
||||
ExtractVersionGuids(calEpisode);
|
||||
ApplyHistoryStatus(calEpisode);
|
||||
|
||||
var episodeMergeKey = (calEpisode.CrSeriesID, calEpisode.AudioLocale);
|
||||
var episodeMergeIndex = episodeMergeIndexByDate[calendarDay.DateTime.Date];
|
||||
var existingEpisode = calendarDay.CalendarEpisodes
|
||||
.FirstOrDefault(e => e.CrSeriesID == calEpisode.CrSeriesID && e.AudioLocale == calEpisode.AudioLocale);
|
||||
|
||||
if (episodeMergeIndex.TryGetValue(episodeMergeKey, out var existingEpisode)){
|
||||
if (existingEpisode != null){
|
||||
if (!int.TryParse(existingEpisode.EpisodeNumber, out _)){
|
||||
existingEpisode.EpisodeNumber = "...";
|
||||
} else{
|
||||
|
|
@ -371,18 +354,17 @@ public class CalendarManager{
|
|||
}
|
||||
|
||||
existingEpisode.CalendarEpisodes.Add(calEpisode);
|
||||
ApplyMergedHistoryStatus(existingEpisode);
|
||||
} else{
|
||||
calendarDay.CalendarEpisodes.Add(calEpisode);
|
||||
episodeMergeIndex[episodeMergeKey] = calEpisode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crunOptions.CalendarShowUpcomingEpisodes){
|
||||
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
|
||||
foreach (var calendarDay in week.CalendarDays){
|
||||
if (calendarDay.DateTime.Date >= nowDate){
|
||||
if (ProgramManager.Instance.AnilistUpcoming.TryGetValue(calendarDay.DateTime.ToString("yyyy-MM-dd"), out var list)){
|
||||
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
|
||||
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
|
||||
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
|
||||
|
||||
foreach (var calendarEpisode in list.Where(calendarEpisodeAnilist => calendarDay.DateTime.Date.Day == calendarEpisodeAnilist.DateTime.Date.Day)
|
||||
.Where(calendarEpisodeAnilist =>
|
||||
|
|
@ -414,176 +396,19 @@ public class CalendarManager{
|
|||
// if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
|
||||
// }
|
||||
|
||||
calendar[calendarKey] = week;
|
||||
RefreshHistoryStatuses(week);
|
||||
calendar["C" + calTargetDate.ToString("yyyy-MM-dd")] = week;
|
||||
|
||||
|
||||
return week;
|
||||
}
|
||||
|
||||
private static void QueueHistoryUpdateFromCalendar(List<CrBrowseEpisode> newEpisodes){
|
||||
Dispatcher.UIThread.Post(async () => {
|
||||
try{
|
||||
await CrunchyrollManager.Instance.History.UpdateWithEpisode(newEpisodes);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
} catch (Exception){
|
||||
Console.Error.WriteLine("Failed to update History from calendar");
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private static void ExtractVersionGuids(CalendarEpisode calEpisode){
|
||||
calEpisode.VersionGuids = calEpisode.Versions?
|
||||
.Select(version => version.Guid)
|
||||
.Where(guid => !string.IsNullOrWhiteSpace(guid))
|
||||
.Select(guid => guid!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList() ?? [];
|
||||
|
||||
var originalVersion = calEpisode.Versions?
|
||||
.FirstOrDefault(version => version.Original);
|
||||
|
||||
calEpisode.OriginalEpisodeGuid = originalVersion?.Guid;
|
||||
calEpisode.OriginalSeasonGuid = originalVersion?.SeasonGuid;
|
||||
}
|
||||
|
||||
private static void RefreshHistoryStatuses(CalendarWeek week){
|
||||
if (week.CalendarDays == null){
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var calendarEpisode in week.CalendarDays.SelectMany(day => day.CalendarEpisodes)){
|
||||
RefreshHistoryStatus(calendarEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RefreshHistoryStatus(CalendarEpisode calendarEpisode){
|
||||
foreach (var childEpisode in calendarEpisode.CalendarEpisodes){
|
||||
ApplyHistoryStatus(childEpisode);
|
||||
}
|
||||
|
||||
ApplyHistoryStatus(calendarEpisode);
|
||||
|
||||
if (calendarEpisode.CalendarEpisodes.Count > 0){
|
||||
ApplyMergedHistoryStatus(calendarEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyHistoryStatus(CalendarEpisode calEpisode){
|
||||
calEpisode.ShowHistoryMark = CrunchyrollManager.Instance.CrunOptions.CalendarShowHistoryMark;
|
||||
calEpisode.HistoryDownloadState = CalendarHistoryDownloadState.None;
|
||||
calEpisode.IsInHistory = false;
|
||||
|
||||
if (!CrunchyrollManager.Instance.CrunOptions.History || string.IsNullOrWhiteSpace(calEpisode.CrSeriesID)){
|
||||
return;
|
||||
}
|
||||
|
||||
var historySeries = CrunchyrollManager.Instance.HistoryList
|
||||
.FirstOrDefault(series => string.Equals(series.SeriesId, calEpisode.CrSeriesID, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (historySeries == null){
|
||||
return;
|
||||
}
|
||||
|
||||
calEpisode.IsInHistory = true;
|
||||
|
||||
var historyMatch = FindHistoryMatch(historySeries, calEpisode);
|
||||
if (historyMatch.HistoryEpisode == null){
|
||||
calEpisode.HistoryDownloadState = CalendarHistoryDownloadState.NotDownloaded;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!historyMatch.HistoryEpisode.WasDownloaded){
|
||||
calEpisode.HistoryDownloadState = CalendarHistoryDownloadState.NotDownloaded;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.HistoryCheckPartialDownloads){
|
||||
var requestedDubs = HistorySeries.GetEffectiveDubLang(historySeries, historyMatch.HistorySeason);
|
||||
var requestedSoftSubs = HistorySeries.GetEffectiveSoftSubs(historySeries, historyMatch.HistorySeason, historyMatch.HistoryEpisode);
|
||||
calEpisode.HistoryDownloadState = historyMatch.HistoryEpisode.IsPartiallyDownloaded(requestedDubs, requestedSoftSubs)
|
||||
? CalendarHistoryDownloadState.PartlyDownloaded
|
||||
: CalendarHistoryDownloadState.Downloaded;
|
||||
} else{
|
||||
calEpisode.HistoryDownloadState = CalendarHistoryDownloadState.Downloaded;
|
||||
}
|
||||
}
|
||||
|
||||
private static (HistorySeason? HistorySeason, HistoryEpisode? HistoryEpisode) FindHistoryMatch(
|
||||
HistorySeries historySeries,
|
||||
CalendarEpisode calEpisode){
|
||||
var candidateSeasonIds = new[]{
|
||||
calEpisode.OriginalSeasonGuid,
|
||||
calEpisode.CrSeasonID
|
||||
}
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Select(id => id!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var candidateEpisodeIds = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(calEpisode.OriginalEpisodeGuid)){
|
||||
candidateEpisodeIds.Add(calEpisode.OriginalEpisodeGuid);
|
||||
}
|
||||
|
||||
candidateEpisodeIds.AddRange(calEpisode.VersionGuids);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(calEpisode.CrEpisodeID)){
|
||||
candidateEpisodeIds.Add(calEpisode.CrEpisodeID);
|
||||
}
|
||||
|
||||
candidateEpisodeIds = candidateEpisodeIds
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
foreach (var historySeason in historySeries.Seasons
|
||||
.Where(historySeason => candidateSeasonIds.Count == 0 || candidateSeasonIds.Contains(historySeason.SeasonId ?? string.Empty, StringComparer.OrdinalIgnoreCase))){
|
||||
var historyEpisode = historySeason.EpisodesList
|
||||
.FirstOrDefault(episode => candidateEpisodeIds.Contains(episode.EpisodeId ?? string.Empty, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
if (historyEpisode != null){
|
||||
return (historySeason, historyEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var historySeason in historySeries.Seasons){
|
||||
var historyEpisode = historySeason.EpisodesList
|
||||
.FirstOrDefault(episode => candidateEpisodeIds.Contains(episode.EpisodeId ?? string.Empty, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
if (historyEpisode != null){
|
||||
return (historySeason, historyEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private static void ApplyMergedHistoryStatus(CalendarEpisode calendarEpisode){
|
||||
var episodes = new[]{ calendarEpisode }
|
||||
.Concat(calendarEpisode.CalendarEpisodes)
|
||||
.Where(episode => episode.IsInHistory)
|
||||
.ToList();
|
||||
|
||||
calendarEpisode.IsInHistory = episodes.Count > 0;
|
||||
|
||||
if (episodes.Count == 0){
|
||||
calendarEpisode.HistoryDownloadState = CalendarHistoryDownloadState.None;
|
||||
} else if (episodes.Any(episode => episode.HistoryDownloadState == CalendarHistoryDownloadState.PartlyDownloaded) ||
|
||||
episodes.Select(episode => episode.HistoryDownloadState).Distinct().Count() > 1){
|
||||
calendarEpisode.HistoryDownloadState = CalendarHistoryDownloadState.PartlyDownloaded;
|
||||
} else{
|
||||
calendarEpisode.HistoryDownloadState = episodes[0].HistoryDownloadState;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task LoadAnilistUpcoming(){
|
||||
DateTime today = DateTime.Today;
|
||||
|
||||
string formattedDate = today.ToString("yyyy-MM-dd");
|
||||
|
||||
if (anilistUpcomingLoadedDate == today || ProgramManager.Instance.AnilistUpcoming.ContainsKey(formattedDate)){
|
||||
anilistUpcomingLoadedDate = today;
|
||||
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(formattedDate)){
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -622,14 +447,9 @@ public class CalendarManager{
|
|||
return;
|
||||
}
|
||||
|
||||
AniListResponseCalendar? currentResponse = Helpers.Deserialize<AniListResponseCalendar>(
|
||||
AniListResponseCalendar currentResponse = Helpers.Deserialize<AniListResponseCalendar>(
|
||||
response.ResponseContent, CrunchyrollManager.Instance.SettingsJsonSerializerSettings
|
||||
);
|
||||
|
||||
if (currentResponse?.Data?.Page == null){
|
||||
Console.Error.WriteLine("Anilist response could not be parsed for upcoming calendar episodes");
|
||||
return;
|
||||
}
|
||||
) ?? new AniListResponseCalendar();
|
||||
|
||||
|
||||
aniListResponse ??= currentResponse;
|
||||
|
|
@ -719,8 +539,6 @@ public class CalendarManager{
|
|||
|
||||
value.Add(calendarEpisode);
|
||||
}
|
||||
|
||||
anilistUpcomingLoadedDate = today;
|
||||
}
|
||||
|
||||
private static void AdjustReleaseTimeToHistory(CalendarEpisode calEp, string crunchyrollId){
|
||||
|
|
@ -762,20 +580,22 @@ public class CalendarManager{
|
|||
}
|
||||
}
|
||||
|
||||
private bool LooksLikeGenericSeasonLabel(string? seasonTitle){
|
||||
if (string.IsNullOrWhiteSpace(seasonTitle))
|
||||
return true;
|
||||
private bool LooksLikeGenericSeasonLabel(string? seasonTitle, double? seasonNo){
|
||||
if (string.IsNullOrWhiteSpace(seasonTitle)) return true;
|
||||
|
||||
var t = seasonTitle.Trim();
|
||||
|
||||
var m = GenericSeasonLabelRegex.Match(t);
|
||||
var m = Regex.Match(t, @"^(?<word>\p{L}+(?:[\p{L}\p{Mn}'\.\- ]*\p{L})?)\s+(?<n>\d+)$",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
if (!m.Success)
|
||||
return false;
|
||||
if (!m.Success) return false;
|
||||
|
||||
var word = m.Groups["word"].Value.Trim();
|
||||
if (seasonNo.HasValue &&
|
||||
double.TryParse(m.Groups["n"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var n)){
|
||||
return n == seasonNo.Value;
|
||||
}
|
||||
|
||||
return word.Equals("Season", StringComparison.OrdinalIgnoreCase);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Query
|
||||
|
|
@ -854,4 +674,4 @@ public class CalendarManager{
|
|||
}";
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -23,13 +23,6 @@ using ReactiveUI;
|
|||
namespace CRD.Downloader.Crunchyroll;
|
||||
|
||||
public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings){
|
||||
private const string UnknownUsername = "???";
|
||||
private const string DefaultAvatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png";
|
||||
private const string CrunchyrollDomain = ".crunchyroll.com";
|
||||
private const string SsoDomain = "sso.crunchyroll.com";
|
||||
private const string DefaultAudioLanguage = "ja-JP";
|
||||
private const string DefaultAnonymousSubtitleLanguage = "de-DE";
|
||||
|
||||
private static readonly TimeSpan TokenRefreshBuffer = TimeSpan.FromSeconds(60);
|
||||
|
||||
public CrToken? Token;
|
||||
|
|
@ -43,83 +36,86 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
|
||||
public Dictionary<string, CookieCollection> cookieStore = new();
|
||||
|
||||
private string authCodeVerifier = string.Empty;
|
||||
private string authCode = string.Empty;
|
||||
|
||||
private bool IsTokenExpiredOrNearExpiry(){
|
||||
return Token == null || DateTime.Now >= Token.expires - TokenRefreshBuffer;
|
||||
}
|
||||
|
||||
public void Init(){
|
||||
Profile = CreateDefaultProfile(crunInstance.DefaultLocale, false);
|
||||
}
|
||||
|
||||
private static CrProfile CreateDefaultProfile(string subtitleLanguage, bool hasPremium = false){
|
||||
return new CrProfile{
|
||||
Username = UnknownUsername,
|
||||
Avatar = DefaultAvatar,
|
||||
PreferredContentAudioLanguage = DefaultAudioLanguage,
|
||||
PreferredContentSubtitleLanguage = subtitleLanguage,
|
||||
HasPremium = hasPremium,
|
||||
Profile = new CrProfile{
|
||||
Username = "???",
|
||||
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
|
||||
PreferredContentAudioLanguage = "ja-JP",
|
||||
PreferredContentSubtitleLanguage = crunInstance.DefaultLocale,
|
||||
HasPremium = false,
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTokenFilePath(){
|
||||
return AuthSettings.Endpoint switch{
|
||||
"tv/samsung" or "tv/vidaa" or "tv/android_tv" => CfgManager.PathCrToken.Replace(".json", "_tv.json"),
|
||||
"android/phone" or "android/tablet" => CfgManager.PathCrToken.Replace(".json", "_android.json"),
|
||||
"console/switch" or "console/ps4" or "console/ps5" or "console/xbox_one" => CfgManager.PathCrToken.Replace(".json", "_console.json"),
|
||||
"---" => CfgManager.PathCrToken.Replace(".json", "_guest.json"),
|
||||
_ => CfgManager.PathCrToken
|
||||
};
|
||||
switch (AuthSettings.Endpoint){
|
||||
case "tv/samsung":
|
||||
case "tv/vidaa":
|
||||
case "tv/android_tv":
|
||||
return CfgManager.PathCrToken.Replace(".json", "_tv.json");
|
||||
case "android/phone":
|
||||
case "android/tablet":
|
||||
return CfgManager.PathCrToken.Replace(".json", "_android.json");
|
||||
case "console/switch":
|
||||
case "console/ps4":
|
||||
case "console/ps5":
|
||||
case "console/xbox_one":
|
||||
return CfgManager.PathCrToken.Replace(".json", "_console.json");
|
||||
case "---":
|
||||
return CfgManager.PathCrToken.Replace(".json", "_guest.json");
|
||||
default:
|
||||
return CfgManager.PathCrToken;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Auth(){
|
||||
var tokenFilePath = GetTokenFilePath();
|
||||
if (CfgManager.CheckIfFileExists(tokenFilePath)){
|
||||
Token = CfgManager.ReadJsonFromFile<CrToken>(tokenFilePath);
|
||||
if (CfgManager.CheckIfFileExists(GetTokenFilePath())){
|
||||
Token = CfgManager.ReadJsonFromFile<CrToken>(GetTokenFilePath());
|
||||
await LoginWithToken();
|
||||
} else{
|
||||
await AuthAnonymous();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Auth(AuthData authData){
|
||||
cookieStore.Clear();
|
||||
if (AuthSettings.Endpoint.StartsWith("tv")){
|
||||
await AuthOld(authData);
|
||||
} else{
|
||||
await AuthCode(authData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SetETPCookie(string refreshToken){
|
||||
HttpClientReq.Instance.AddCookie(CrunchyrollDomain, new Cookie("etp_rt", refreshToken), cookieStore);
|
||||
HttpClientReq.Instance.AddCookie(CrunchyrollDomain, new Cookie("c_locale", "en-US"), cookieStore);
|
||||
HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken), cookieStore);
|
||||
HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"), cookieStore);
|
||||
}
|
||||
|
||||
public Task AuthAnonymous(){
|
||||
return AuthAnonymousInternal(true);
|
||||
}
|
||||
public async Task AuthAnonymous(){
|
||||
string uuid = string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||
|
||||
public Task AuthAnonymousFoxy(){
|
||||
return AuthAnonymousInternal(false);
|
||||
}
|
||||
|
||||
private async Task AuthAnonymousInternal(bool includeDeviceMetadata){
|
||||
var uuid = ResolveDeviceId();
|
||||
Subscription = new Subscription();
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "grant_type", "client_id" },
|
||||
{ "scope", "offline_access" },
|
||||
{ "device_id", uuid },
|
||||
{ "device_type", AuthSettings.Device_type },
|
||||
};
|
||||
|
||||
if (includeDeviceMetadata){
|
||||
Subscription = new Subscription();
|
||||
formData["scope"] = "offline_access";
|
||||
AddDeviceMetadata(formData, uuid);
|
||||
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||
formData.Add("device_name", AuthSettings.Device_name);
|
||||
}
|
||||
|
||||
var requestContent = new FormUrlEncodedContent(formData);
|
||||
|
||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||
{ "Authorization", AuthSettings.Authorization },
|
||||
{ "User-Agent", AuthSettings.UserAgent }
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||
Content = requestContent
|
||||
};
|
||||
|
||||
foreach (var header in crunchyAuthHeaders){
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
var request = CreateTokenRequest(formData);
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (response.IsOk){
|
||||
|
|
@ -128,193 +124,129 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
Console.Error.WriteLine("Anonymous login failed");
|
||||
}
|
||||
|
||||
Profile = CreateDefaultProfile(DefaultAnonymousSubtitleLanguage);
|
||||
Profile = new CrProfile{
|
||||
Username = "???",
|
||||
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
|
||||
PreferredContentAudioLanguage = "ja-JP",
|
||||
PreferredContentSubtitleLanguage = "de-DE"
|
||||
};
|
||||
}
|
||||
|
||||
private void JsonTokenToFileAndVariable(string content, string deviceId){
|
||||
Token = Helpers.Deserialize<CrToken>(content, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
if (Token is not{ expires_in: not null }){
|
||||
return;
|
||||
if (Token is{ expires_in: not null }){
|
||||
Token.device_id = deviceId;
|
||||
Token.expires = DateTime.Now.AddSeconds((double)Token.expires_in);
|
||||
|
||||
if (EndpointEnum == CrunchyrollEndpoints.Guest){
|
||||
return;
|
||||
}
|
||||
|
||||
CfgManager.WriteJsonToFile(GetTokenFilePath(), Token);
|
||||
}
|
||||
|
||||
Token.device_id = deviceId;
|
||||
Token.expires = DateTime.Now.AddSeconds((double)Token.expires_in);
|
||||
NotificationPublisher.Instance.ResetLoginExpiredNotification();
|
||||
|
||||
if (EndpointEnum == CrunchyrollEndpoints.Guest){
|
||||
return;
|
||||
}
|
||||
|
||||
CfgManager.WriteJsonToFile(GetTokenFilePath(), Token);
|
||||
}
|
||||
|
||||
private async Task AuthCode(AuthData authData){
|
||||
var uuid = ResolveDeviceId();
|
||||
var loginPayload = JsonConvert.SerializeObject(new Dictionary<string, object>{
|
||||
{ "email", authData.Username },
|
||||
{ "password", authData.Password },
|
||||
{ "eventSettings", new Dictionary<string, object>() }
|
||||
});
|
||||
var requestContent = new StringContent(loginPayload, Encoding.UTF8);
|
||||
requestContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain"){
|
||||
CharSet = "UTF-8"
|
||||
};
|
||||
|
||||
var request = CreateRequest(HttpMethod.Post, "https://sso.crunchyroll.com/api/login", requestContent, includeAuthorization: false);
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore);
|
||||
|
||||
if (response.IsOk){
|
||||
var refreshToken = HttpClientReq.Instance.GetCookieValue(SsoDomain, "etp_rt", cookieStore);
|
||||
Token = new CrToken{ refresh_token = refreshToken, device_id = uuid };
|
||||
await GetCodeAuth();
|
||||
await LoginWithCode();
|
||||
} else{
|
||||
await PublishLoginFailureAsync(response.ResponseContent);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateCodeVerifier(int length = 64){
|
||||
// RFC 7636: length between 43 and 128.
|
||||
const string allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
||||
var bytes = RandomNumberGenerator.GetBytes(length);
|
||||
var stringBuilder = new StringBuilder(length);
|
||||
|
||||
foreach (var value in bytes){
|
||||
stringBuilder.Append(allowed[value % allowed.Length]);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public async Task GetCodeAuth(){
|
||||
var uuid = Guid.NewGuid().ToString();
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
authCodeVerifier = GenerateCodeVerifier();
|
||||
var clientId = GetClientIdFromBasicHeader(AuthSettings.Authorization);
|
||||
|
||||
query["client_id"] = clientId;
|
||||
query["redirect_uri"] = "sso.crunchyroll://auth";
|
||||
query["response_type"] = "code";
|
||||
query["scope"] = "offline_access";
|
||||
query["state"] = "{\"flow\":\"SIGN_IN\",\"flowRoot\":\"ONBOARDING\"}";
|
||||
query["code_challenge"] = authCodeVerifier;
|
||||
query["code_challenge_method"] = "plain";
|
||||
|
||||
HttpClientReq.Instance.AddCookie(SsoDomain, new Cookie("client_id", clientId), cookieStore);
|
||||
HttpClientReq.Instance.AddCookie(SsoDomain, new Cookie("device_id", uuid), cookieStore);
|
||||
|
||||
var uriBuilder = new UriBuilder("https://sso.crunchyroll.com/authorize"){
|
||||
Query = query.ToString()
|
||||
};
|
||||
|
||||
var request = CreateRequest(HttpMethod.Get, uriBuilder.ToString(), includeAuthorization: false);
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
authCode = ExtractCode(response.ResponseContent);
|
||||
|
||||
if (string.IsNullOrEmpty(authCode)){
|
||||
Console.Error.WriteLine("Auth code is empty");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetClientIdFromBasicHeader(string authorizationHeader){
|
||||
if (string.IsNullOrWhiteSpace(authorizationHeader)){
|
||||
throw new ArgumentException("Authorization header is null/empty.", nameof(authorizationHeader));
|
||||
}
|
||||
|
||||
const string prefix = "Basic ";
|
||||
if (!authorizationHeader.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)){
|
||||
throw new FormatException("Authorization header is not Basic.");
|
||||
}
|
||||
|
||||
var base64 = authorizationHeader[prefix.Length..].Trim();
|
||||
|
||||
byte[] bytes;
|
||||
try{
|
||||
bytes = Convert.FromBase64String(base64);
|
||||
} catch (FormatException ex){
|
||||
throw new FormatException("Basic token is not valid Base64.", ex);
|
||||
}
|
||||
|
||||
var decoded = Encoding.UTF8.GetString(bytes);
|
||||
var separatorIndex = decoded.IndexOf(':');
|
||||
if (separatorIndex <= 0){
|
||||
throw new FormatException("Decoded Basic value is not in 'clientId:clientSecret' format.");
|
||||
}
|
||||
|
||||
return decoded[..separatorIndex];
|
||||
}
|
||||
|
||||
private static string Normalize(string value){
|
||||
value = Regex.Unescape(value);
|
||||
value = value.Replace(@"\u0026", "&");
|
||||
value = value.Replace("\\\"", "\"");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string ExtractCode(string body){
|
||||
var text = Normalize(body);
|
||||
|
||||
var match = Regex.Match(text, @"(?:[?&]|\\u0026)code=([A-Za-z0-9\-_]+)");
|
||||
if (match.Success){
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
match = Regex.Match(text, @"code=([A-Za-z0-9\-_]+)");
|
||||
if (match.Success){
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("Authorization code not found in response body.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private async Task AuthOld(AuthData data){
|
||||
var uuid = Guid.NewGuid().ToString();
|
||||
string uuid = Guid.NewGuid().ToString();
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "username", data.Username },
|
||||
{ "password", data.Password },
|
||||
{ "grant_type", "password" },
|
||||
{ "scope", "offline_access" },
|
||||
{ "device_id", uuid },
|
||||
{ "device_type", AuthSettings.Device_type },
|
||||
};
|
||||
AddDeviceMetadata(formData, uuid);
|
||||
|
||||
var request = CreateTokenRequest(formData);
|
||||
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||
formData.Add("device_name", AuthSettings.Device_name);
|
||||
}
|
||||
|
||||
var requestContent = new FormUrlEncodedContent(formData);
|
||||
|
||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||
{ "Authorization", AuthSettings.Authorization },
|
||||
{ "User-Agent", AuthSettings.UserAgent }
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||
Content = requestContent
|
||||
};
|
||||
|
||||
foreach (var header in crunchyAuthHeaders){
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (response.IsOk){
|
||||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||
} else{
|
||||
await PublishLoginFailureAsync(response.ResponseContent);
|
||||
if (response.ResponseContent.Contains("invalid_credentials")){
|
||||
MessageBus.Current.SendMessage(new ToastMessage("Login failed. Please check your username and password.", ToastType.Error, 5));
|
||||
} else if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error {(crunInstance.CrunOptions.UseCrBetaApi ? "" : "try to change to BetaAPI in settings")}", ToastType.Error, 5));
|
||||
} else{
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0, response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}",
|
||||
ToastType.Error, 5));
|
||||
await Console.Error.WriteLineAsync("Full Response: " + response.ResponseContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (Token?.refresh_token != null){
|
||||
SetETPCookie(Token.refresh_token);
|
||||
|
||||
await GetMultiProfile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task ChangeProfile(string profileId){
|
||||
if (HasNoUsableRefreshToken()){
|
||||
if (Token?.access_token == null && Token?.refresh_token == null ||
|
||||
Token.access_token != null && Token.refresh_token == null){
|
||||
await AuthAnonymous();
|
||||
}
|
||||
|
||||
if (Profile.Username == UnknownUsername || string.IsNullOrEmpty(profileId) || Token?.refresh_token == null){
|
||||
if (Profile.Username == "???"){
|
||||
return;
|
||||
}
|
||||
|
||||
var uuid = ResolveDeviceId();
|
||||
if (string.IsNullOrEmpty(profileId) || Token?.refresh_token == null){
|
||||
return;
|
||||
}
|
||||
|
||||
string uuid = string.IsNullOrEmpty(Token.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||
|
||||
SetETPCookie(Token.refresh_token);
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "grant_type", "refresh_token_profile_id" },
|
||||
{ "profile_id", profileId },
|
||||
{ "device_id", uuid },
|
||||
{ "device_type", AuthSettings.Device_type },
|
||||
};
|
||||
AddDeviceMetadata(formData, uuid);
|
||||
|
||||
var request = CreateTokenRequest(formData);
|
||||
var requestContent = new FormUrlEncodedContent(formData);
|
||||
|
||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||
{ "Authorization", AuthSettings.Authorization },
|
||||
{ "User-Agent", AuthSettings.UserAgent }
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||
Content = requestContent
|
||||
};
|
||||
|
||||
foreach (var header in crunchyAuthHeaders){
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (Token?.refresh_token != null) SetETPCookie(Token.refresh_token);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore);
|
||||
|
||||
if (response.IsOk){
|
||||
|
|
@ -324,6 +256,7 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
}
|
||||
|
||||
await GetMultiProfile();
|
||||
|
||||
} else{
|
||||
Console.Error.WriteLine("Refresh Token Auth Failed");
|
||||
}
|
||||
|
|
@ -336,6 +269,7 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
}
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage(ApiUrls.Profile, HttpMethod.Get, true, Token.access_token, null);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (response.IsOk){
|
||||
|
|
@ -343,6 +277,7 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
|
||||
if (profileTemp != null){
|
||||
Profile = profileTemp;
|
||||
|
||||
await GetSubscription();
|
||||
}
|
||||
}
|
||||
|
|
@ -350,40 +285,40 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
|
||||
private async Task GetSubscription(){
|
||||
var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + Token.account_id, HttpMethod.Get, true, Token.access_token, null);
|
||||
|
||||
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
|
||||
|
||||
if (!responseSubs.IsOk){
|
||||
Profile.HasPremium = false;
|
||||
Console.Error.WriteLine("Failed to check premium subscription status");
|
||||
return;
|
||||
}
|
||||
|
||||
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
Subscription = subsc;
|
||||
if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
|
||||
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
|
||||
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
|
||||
var remaining = expiration - DateTime.Now;
|
||||
Profile.HasPremium = true;
|
||||
if (Subscription != null){
|
||||
Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||
Subscription.NextRenewalDate = expiration;
|
||||
if (responseSubs.IsOk){
|
||||
var subsc = Helpers.Deserialize<Subscription>(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
Subscription = subsc;
|
||||
if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){
|
||||
var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First();
|
||||
var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate;
|
||||
var remaining = expiration - DateTime.Now;
|
||||
Profile.HasPremium = true;
|
||||
if (Subscription != null){
|
||||
Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||
Subscription.NextRenewalDate = expiration;
|
||||
}
|
||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
|
||||
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
|
||||
var remaining = nonRecurringSub.EndDate - DateTime.Now;
|
||||
Profile.HasPremium = true;
|
||||
if (Subscription != null){
|
||||
Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||
Subscription.NextRenewalDate = nonRecurringSub.EndDate;
|
||||
}
|
||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){
|
||||
Profile.HasPremium = true;
|
||||
} else if (subsc is{ SubscriptionProducts.Count: > 0 }){
|
||||
Profile.HasPremium = true;
|
||||
} else{
|
||||
Profile.HasPremium = false;
|
||||
Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} ");
|
||||
}
|
||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){
|
||||
var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First();
|
||||
var remaining = nonRecurringSub.EndDate - DateTime.Now;
|
||||
Profile.HasPremium = true;
|
||||
if (Subscription != null){
|
||||
Subscription.IsActive = remaining > TimeSpan.Zero;
|
||||
Subscription.NextRenewalDate = nonRecurringSub.EndDate;
|
||||
}
|
||||
} else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){
|
||||
Profile.HasPremium = true;
|
||||
} else if (subsc is{ SubscriptionProducts.Count: > 0 }){
|
||||
Profile.HasPremium = true;
|
||||
} else{
|
||||
Profile.HasPremium = false;
|
||||
Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} ");
|
||||
Console.Error.WriteLine("Failed to check premium subscription status");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -393,42 +328,21 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
return;
|
||||
}
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage(ApiUrls.MultiProfile, HttpMethod.Get, true, Token.access_token);
|
||||
var request = HttpClientReq.CreateRequestMessage(ApiUrls.MultiProfile, HttpMethod.Get, true, Token?.access_token);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore);
|
||||
|
||||
if (response.IsOk){
|
||||
MultiProfile = Helpers.Deserialize<CrMultiProfile>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrMultiProfile();
|
||||
|
||||
var selectedProfile = MultiProfile.Profiles.FirstOrDefault(e => e.IsSelected);
|
||||
if (selectedProfile != null){
|
||||
Profile = selectedProfile;
|
||||
}
|
||||
|
||||
var selectedProfile = MultiProfile.Profiles.FirstOrDefault( e => e.IsSelected);
|
||||
if (selectedProfile != null) Profile = selectedProfile;
|
||||
|
||||
await GetSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoginWithCode(){
|
||||
if (string.IsNullOrEmpty(authCode)){
|
||||
Console.Error.WriteLine("Missing code");
|
||||
await AuthAnonymous();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var uuid = ResolveDeviceId();
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "code", authCode },
|
||||
{ "code_verifier", authCodeVerifier },
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "scope", "offline_access" },
|
||||
};
|
||||
AddDeviceMetadata(formData, uuid);
|
||||
|
||||
var request = CreateTokenRequest(formData);
|
||||
SetETPCookie(Token?.refresh_token ?? string.Empty);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
await CompleteInteractiveTokenLoginAsync(response, uuid);
|
||||
}
|
||||
|
||||
public async Task LoginWithToken(){
|
||||
|
|
@ -438,12 +352,62 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
return;
|
||||
}
|
||||
|
||||
var uuid = ResolveDeviceId();
|
||||
var request = CreateRefreshTokenRequest(uuid, Token.refresh_token);
|
||||
string uuid = string.IsNullOrEmpty(Token.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "refresh_token", Token.refresh_token },
|
||||
{ "scope", "offline_access" },
|
||||
{ "device_id", uuid },
|
||||
{ "grant_type", "refresh_token" },
|
||||
{ "device_type", AuthSettings.Device_type },
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||
formData.Add("device_name", AuthSettings.Device_name);
|
||||
}
|
||||
|
||||
var requestContent = new FormUrlEncodedContent(formData);
|
||||
|
||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||
{ "Authorization", AuthSettings.Authorization },
|
||||
{ "User-Agent", AuthSettings.UserAgent }
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||
Content = requestContent
|
||||
};
|
||||
|
||||
foreach (var header in crunchyAuthHeaders){
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
SetETPCookie(Token.refresh_token);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
await CompleteInteractiveTokenLoginAsync(response, uuid);
|
||||
|
||||
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error {(crunInstance.CrunOptions.UseCrBetaApi ? "" : "try to change to BetaAPI in settings")}", ToastType.Error, 5));
|
||||
Console.Error.WriteLine($"Failed to login - Cloudflare error {(crunInstance.CrunOptions.UseCrBetaApi ? "" : "try to change to BetaAPI in settings")}");
|
||||
}
|
||||
|
||||
if (response.IsOk){
|
||||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||
|
||||
if (Token?.refresh_token != null){
|
||||
SetETPCookie(Token.refresh_token);
|
||||
|
||||
await GetMultiProfile();
|
||||
}
|
||||
} else{
|
||||
Console.Error.WriteLine("Token Auth Failed");
|
||||
await AuthAnonymous();
|
||||
|
||||
MainWindow.Instance.ShowError("Login failed. Please check the log for more details.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshToken(bool needsToken){
|
||||
|
|
@ -456,22 +420,52 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
return;
|
||||
}
|
||||
|
||||
if (HasNoUsableRefreshToken()){
|
||||
if (Token?.access_token == null && Token?.refresh_token == null ||
|
||||
Token.access_token != null && Token.refresh_token == null){
|
||||
await AuthAnonymous();
|
||||
} else if (!IsTokenExpiredOrNearExpiry() && needsToken){
|
||||
} else{
|
||||
if (!IsTokenExpiredOrNearExpiry() && needsToken){
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (Profile.Username == "???"){
|
||||
return;
|
||||
}
|
||||
|
||||
if (Profile.Username == UnknownUsername){
|
||||
return;
|
||||
var hadUserSession = !string.IsNullOrWhiteSpace(Token?.refresh_token) && !string.IsNullOrWhiteSpace(Profile.Username) && Profile.Username != "???";
|
||||
|
||||
string uuid = string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "refresh_token", Token?.refresh_token ?? "" },
|
||||
{ "grant_type", "refresh_token" },
|
||||
{ "scope", "offline_access" },
|
||||
{ "device_id", uuid },
|
||||
{ "device_type", AuthSettings.Device_type },
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||
formData.Add("device_name", AuthSettings.Device_name);
|
||||
}
|
||||
|
||||
var hadUserSession = !string.IsNullOrWhiteSpace(Token?.refresh_token) && !string.IsNullOrWhiteSpace(Profile.Username) && Profile.Username != UnknownUsername;
|
||||
var uuid = ResolveDeviceId();
|
||||
var refreshToken = Token?.refresh_token ?? string.Empty;
|
||||
var request = CreateRefreshTokenRequest(uuid, refreshToken);
|
||||
var requestContent = new FormUrlEncodedContent(formData);
|
||||
|
||||
var crunchyAuthHeaders = new Dictionary<string, string>{
|
||||
{ "Authorization", AuthSettings.Authorization },
|
||||
{ "User-Agent", AuthSettings.UserAgent }
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){
|
||||
Content = requestContent
|
||||
};
|
||||
|
||||
foreach (var header in crunchyAuthHeaders){
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
SetETPCookie(Token?.refresh_token ?? string.Empty);
|
||||
|
||||
SetETPCookie(refreshToken);
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (response.IsOk){
|
||||
|
|
@ -483,95 +477,4 @@ public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteInteractiveTokenLoginAsync((bool IsOk, string ResponseContent, string error, Dictionary<string, string> Headers) response, string uuid){
|
||||
if (IsCloudflareBlock(response.ResponseContent)){
|
||||
PublishCloudflareLoginFailure();
|
||||
}
|
||||
|
||||
if (response.IsOk){
|
||||
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
|
||||
|
||||
if (Token?.refresh_token != null){
|
||||
SetETPCookie(Token.refresh_token);
|
||||
await GetMultiProfile();
|
||||
}
|
||||
} else{
|
||||
Console.Error.WriteLine("Token Auth Failed");
|
||||
await AuthAnonymous();
|
||||
MainWindow.Instance.ShowError("Login failed. Please check the log for more details.");
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRefreshTokenRequest(string uuid, string refreshToken){
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "refresh_token", refreshToken },
|
||||
{ "scope", "offline_access" },
|
||||
{ "grant_type", "refresh_token" },
|
||||
};
|
||||
AddDeviceMetadata(formData, uuid);
|
||||
return CreateTokenRequest(formData);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateTokenRequest(Dictionary<string, string> formData){
|
||||
return CreateRequest(HttpMethod.Post, ApiUrls.Auth, new FormUrlEncodedContent(formData));
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string uri, HttpContent? content = null, bool includeAuthorization = true){
|
||||
var request = new HttpRequestMessage(method, uri){
|
||||
Content = content
|
||||
};
|
||||
|
||||
if (includeAuthorization){
|
||||
request.Headers.Add("Authorization", AuthSettings.Authorization);
|
||||
}
|
||||
|
||||
request.Headers.Add("User-Agent", AuthSettings.UserAgent);
|
||||
return request;
|
||||
}
|
||||
|
||||
private void AddDeviceMetadata(Dictionary<string, string> formData, string uuid){
|
||||
formData["device_id"] = uuid;
|
||||
formData["device_type"] = AuthSettings.Device_type;
|
||||
|
||||
if (!string.IsNullOrEmpty(AuthSettings.Device_name)){
|
||||
formData["device_name"] = AuthSettings.Device_name;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveDeviceId(){
|
||||
return string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id;
|
||||
}
|
||||
|
||||
private bool HasNoUsableRefreshToken(){
|
||||
return Token?.access_token == null && Token?.refresh_token == null ||
|
||||
Token?.access_token != null && Token.refresh_token == null;
|
||||
}
|
||||
|
||||
private async Task PublishLoginFailureAsync(string responseContent){
|
||||
if (responseContent.Contains("invalid_credentials")){
|
||||
MessageBus.Current.SendMessage(new ToastMessage("Login failed. Please check your username and password.", ToastType.Error, 5));
|
||||
} else if (IsCloudflareBlock(responseContent)){
|
||||
PublishCloudflareLoginFailure();
|
||||
} else{
|
||||
var previewLength = Math.Min(responseContent.Length, 200);
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {responseContent[..previewLength]}", ToastType.Error, 5));
|
||||
await Console.Error.WriteLineAsync("Full Response: " + responseContent);
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishCloudflareLoginFailure(){
|
||||
var betaApiHint = crunInstance.CrunOptions.UseCrBetaApi ? string.Empty : "try to change to BetaAPI in settings";
|
||||
var message = $"Failed to login - Cloudflare error {betaApiHint}";
|
||||
MessageBus.Current.SendMessage(new ToastMessage(message, ToastType.Error, 5));
|
||||
Console.Error.WriteLine(message);
|
||||
}
|
||||
|
||||
private static bool IsCloudflareBlock(string responseContent){
|
||||
return responseContent.Contains("<title>Just a moment...</title>") ||
|
||||
responseContent.Contains("<title>Access denied</title>") ||
|
||||
responseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
responseContent.Trim().Equals("error code: 1020") ||
|
||||
responseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public class CrEpisode(){
|
|||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Episode request failed for id '{id}'");
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -253,67 +253,20 @@ public class CrEpisode(){
|
|||
}
|
||||
}
|
||||
|
||||
query["n"] = requestAmount + "";
|
||||
query["sort_by"] = "newly_added";
|
||||
query["type"] = "episode";
|
||||
|
||||
if (requestAmount <= 0){
|
||||
return new CrBrowseEpisodeBase{
|
||||
Data = []
|
||||
};
|
||||
}
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthGuest.Token?.access_token, query);
|
||||
|
||||
const int maxPageSize = 100;
|
||||
const int stalePageTolerance = 3;
|
||||
CrBrowseEpisodeBase? series = null;
|
||||
var episodes = new List<CrBrowseEpisode>();
|
||||
var stalePageCount = 0;
|
||||
var firstWeekDayDate = firstWeekDay?.Date;
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
for (var start = 0; start < requestAmount; start += maxPageSize){
|
||||
var pageSize = Math.Min(maxPageSize, requestAmount - start);
|
||||
query["start"] = start.ToString();
|
||||
query["n"] = pageSize.ToString();
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthGuest.Token?.access_token, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"New episodes request failed for start '{start}' and n '{pageSize}'");
|
||||
return null;
|
||||
}
|
||||
|
||||
var page = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
series ??= page;
|
||||
|
||||
if (page?.Data is not{ Count: > 0 } pageData){
|
||||
break;
|
||||
}
|
||||
|
||||
episodes.AddRange(pageData);
|
||||
|
||||
if (firstWeekDayDate.HasValue){
|
||||
if (pageData.Any(episode => GetCalendarTargetDate(episode).Date >= firstWeekDayDate.Value)){
|
||||
stalePageCount = 0;
|
||||
} else{
|
||||
stalePageCount++;
|
||||
|
||||
if (stalePageCount >= stalePageTolerance){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pageData.Count < pageSize){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (series == null){
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
series.Data = episodes;
|
||||
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
|
||||
|
||||
series?.Data?.Sort((a, b) =>
|
||||
b.EpisodeMetadata.PremiumAvailableDate.CompareTo(a.EpisodeMetadata.PremiumAvailableDate));
|
||||
|
|
@ -321,31 +274,6 @@ public class CrEpisode(){
|
|||
return series;
|
||||
}
|
||||
|
||||
private static DateTime GetCalendarTargetDate(CrBrowseEpisode episode){
|
||||
DateTime episodeAirDate = episode.EpisodeMetadata.EpisodeAirDate.Kind == DateTimeKind.Utc
|
||||
? episode.EpisodeMetadata.EpisodeAirDate.ToLocalTime()
|
||||
: episode.EpisodeMetadata.EpisodeAirDate;
|
||||
|
||||
DateTime premiumAvailableStart = episode.EpisodeMetadata.PremiumAvailableDate.Kind == DateTimeKind.Utc
|
||||
? episode.EpisodeMetadata.PremiumAvailableDate.ToLocalTime()
|
||||
: episode.EpisodeMetadata.PremiumAvailableDate;
|
||||
|
||||
DateTime targetDate = premiumAvailableStart;
|
||||
DateTime oneYearFromNow = DateTime.Now.AddYears(1);
|
||||
|
||||
if (targetDate >= oneYearFromNow){
|
||||
DateTime freeAvailableStart = episode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
|
||||
? episode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
|
||||
: episode.EpisodeMetadata.FreeAvailableDate;
|
||||
|
||||
targetDate = freeAvailableStart <= oneYearFromNow
|
||||
? freeAvailableStart
|
||||
: episodeAirDate;
|
||||
}
|
||||
|
||||
return targetDate;
|
||||
}
|
||||
|
||||
public async Task MarkAsWatched(string episodeId){
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true,
|
||||
crunInstance.CrAuthEndpoint1.Token?.access_token, null);
|
||||
|
|
@ -356,4 +284,4 @@ public class CrEpisode(){
|
|||
Console.Error.WriteLine($"Mark as watched for {episodeId} failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ 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.Downloader.Crunchyroll.Utils;
|
||||
|
|
@ -25,11 +26,11 @@ public class CrSeries{
|
|||
|
||||
var hslang = crunInstance.CrunOptions.Hslang;
|
||||
|
||||
bool ShouldInclude(string selectionKey) =>
|
||||
all is true || (e != null && e.Contains(selectionKey));
|
||||
bool ShouldInclude(string epNum) =>
|
||||
all is true || (e != null && e.Contains(epNum));
|
||||
|
||||
foreach (var (key, episode) in eps){
|
||||
var epNum = GetEpisodeLabelFromKey(key);
|
||||
var epNum = key.StartsWith('E') ? key[1..] : key;
|
||||
|
||||
foreach (var v in episode.Variants){
|
||||
var item = v.Item;
|
||||
|
|
@ -69,7 +70,7 @@ public class CrSeries{
|
|||
}
|
||||
|
||||
// selection gate
|
||||
if (!ShouldInclude(key))
|
||||
if (!ShouldInclude(epNum))
|
||||
continue;
|
||||
|
||||
// Create base queue item once per "key"
|
||||
|
|
@ -132,7 +133,7 @@ public class CrSeries{
|
|||
}
|
||||
|
||||
|
||||
public async Task<CrunchySeriesList?> ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data, bool forcedLocale = false, bool updateHistory = true){
|
||||
public async Task<CrunchySeriesList?> ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data, bool forcedLocale = false){
|
||||
bool serieshasversions = true;
|
||||
|
||||
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale, forcedLocale);
|
||||
|
|
@ -144,7 +145,7 @@ public class CrSeries{
|
|||
|
||||
var episodes = new Dictionary<string, EpisodeAndLanguage>();
|
||||
|
||||
if (crunInstance.CrunOptions.History && updateHistory)
|
||||
if (crunInstance.CrunOptions.History)
|
||||
_ = crunInstance.History.CrUpdateSeries(id, "");
|
||||
|
||||
var cachedSeasonId = "";
|
||||
|
|
@ -169,7 +170,7 @@ public class CrSeries{
|
|||
(episode.Episode != string.Empty ? episode.Episode : (episode.EpisodeNumber != null ? episode.EpisodeNumber + "" : $"F{fallbackIndex++}"))
|
||||
?? string.Empty;
|
||||
|
||||
var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier) && s.Identifier.Contains('|')
|
||||
var seasonIdentifier = !string.IsNullOrEmpty(s.Identifier)
|
||||
? s.Identifier.Split('|')[1]
|
||||
: $"S{episode.SeasonNumber}";
|
||||
|
||||
|
|
@ -193,7 +194,7 @@ public class CrSeries{
|
|||
}
|
||||
}
|
||||
|
||||
if (crunInstance.CrunOptions.History && updateHistory){
|
||||
if (crunInstance.CrunOptions.History){
|
||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == id);
|
||||
if (historySeries != null){
|
||||
crunInstance.History.MatchHistorySeriesWithSonarr(false);
|
||||
|
|
@ -214,18 +215,14 @@ public class CrSeries{
|
|||
|
||||
var baseEp = item.Variants[0].Item;
|
||||
|
||||
var isSpecial = baseEp.IsSpecialEpisode();
|
||||
var epStr = baseEp.Episode;
|
||||
var isSpecial = epStr != null && !Regex.IsMatch(epStr, @"^\d+(\.\d+)?$");
|
||||
|
||||
string newKey;
|
||||
if (isSpecial && !string.IsNullOrEmpty(baseEp.Episode)){
|
||||
newKey = $"SP{specialIndex}_" + baseEp.Episode;
|
||||
} else{
|
||||
var episodeLabel = CrunchyEpisode.IsRegularEpisodeNumber(baseEp.Episode)
|
||||
? baseEp.Episode
|
||||
: epIndex.ToString();
|
||||
var separatorIndex = key.LastIndexOf('E');
|
||||
var keyPrefix = separatorIndex > 0 ? key[..separatorIndex] : string.Empty;
|
||||
newKey = $"{keyPrefix}E{episodeLabel}";
|
||||
newKey = $"{(isSpecial ? "SP" : 'E')}{(isSpecial ? (specialIndex + " " + baseEp.Id) : epIndex + "")}";
|
||||
}
|
||||
|
||||
episodes.Remove(key);
|
||||
|
|
@ -243,7 +240,7 @@ public class CrSeries{
|
|||
else epIndex++;
|
||||
}
|
||||
|
||||
var normal = episodes.Where(kvp => !kvp.Key.StartsWith("SP")).ToList();
|
||||
var normal = episodes.Where(kvp => kvp.Key.StartsWith("E")).ToList();
|
||||
var specials = episodes.Where(kvp => kvp.Key.StartsWith("SP")).ToList();
|
||||
|
||||
var sortedEpisodes = new Dictionary<string, EpisodeAndLanguage>(normal.Concat(specials));
|
||||
|
|
@ -262,7 +259,7 @@ public class CrSeries{
|
|||
);
|
||||
|
||||
var title = baseEp.Title;
|
||||
var seasonNumber = GetSeasonDisplaySuffix(baseEp);
|
||||
var seasonNumber = Helpers.ExtractNumberAfterS(baseEp.Identifier) ?? baseEp.SeasonNumber.ToString();
|
||||
|
||||
var languages = item.Variants
|
||||
.Select(v => $"{(v.Item.IsPremiumOnly ? "+ " : "")}{v.Lang?.Name ?? "Unknown"}")
|
||||
|
|
@ -284,7 +281,7 @@ public class CrSeries{
|
|||
|
||||
if (value.Variants.Count == 0){
|
||||
return new Episode{
|
||||
E = key,
|
||||
E = key.StartsWith("E") ? key.Substring(1) : key,
|
||||
Lang = new List<string>(),
|
||||
Name = string.Empty,
|
||||
Season = string.Empty,
|
||||
|
|
@ -314,15 +311,15 @@ public class CrSeries{
|
|||
Languages.SortListByLangList(langList);
|
||||
|
||||
return new Episode{
|
||||
E = key,
|
||||
E = key.StartsWith("E") ? key.Substring(1) : key,
|
||||
Lang = langList,
|
||||
Name = baseEp.Title ?? string.Empty,
|
||||
Season = GetSeasonDisplaySuffix(baseEp),
|
||||
Season = (Helpers.ExtractNumberAfterS(baseEp.Identifier) ?? baseEp.SeasonNumber.ToString()) ?? string.Empty,
|
||||
SeriesTitle = DownloadQueueItemFactory.StripDubSuffix(baseEp.SeriesTitle),
|
||||
SeasonTitle = DownloadQueueItemFactory.StripDubSuffix(baseEp.SeasonTitle),
|
||||
EpisodeNum = key.StartsWith("SP")
|
||||
? key
|
||||
: GetEpisodeLabelFromKey(key),
|
||||
: (baseEp.EpisodeNumber?.ToString() ?? baseEp.Episode ?? "?"),
|
||||
Id = baseEp.SeasonId ?? string.Empty,
|
||||
Img = img,
|
||||
Description = baseEp.Description ?? string.Empty,
|
||||
|
|
@ -334,20 +331,6 @@ public class CrSeries{
|
|||
return crunchySeriesList;
|
||||
}
|
||||
|
||||
private static string GetEpisodeLabelFromKey(string key){
|
||||
if (key.StartsWith("SP"))
|
||||
return key;
|
||||
|
||||
var separatorIndex = key.LastIndexOf('E');
|
||||
return separatorIndex >= 0 && separatorIndex < key.Length - 1
|
||||
? key[(separatorIndex + 1)..]
|
||||
: key;
|
||||
}
|
||||
|
||||
private static string GetSeasonDisplaySuffix(CrunchyEpisode episode){
|
||||
return Helpers.ExtractNumberAfterS(episode.Identifier) ?? episode.SeasonNumber.ToString();
|
||||
}
|
||||
|
||||
public async Task<CrunchyEpisodeList> GetSeasonDataById(string seasonId, string? crLocale, bool forcedLang = false, bool log = false){
|
||||
await crunInstance.CrAuthGuest.RefreshToken(true);
|
||||
CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List<CrunchyEpisode>(), Total = 0, Meta = new Meta() };
|
||||
|
|
@ -421,7 +404,7 @@ public class CrSeries{
|
|||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Series seasons request failed for series id '{id}'");
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -454,7 +437,7 @@ public class CrSeries{
|
|||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Series details request failed for series id '{id}'");
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -490,7 +473,7 @@ public class CrSeries{
|
|||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Series search request failed for query '{searchString}'");
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -536,7 +519,7 @@ public class CrSeries{
|
|||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"All series browse request failed for start '{i}'");
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -572,7 +555,7 @@ public class CrSeries{
|
|||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
if (!response.IsOk){
|
||||
Console.Error.WriteLine($"Seasonal series request failed for '{season}-{year}'");
|
||||
Console.Error.WriteLine("Series Request Failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -580,4 +563,4 @@ public class CrSeries{
|
|||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,9 +68,6 @@ public class CrunchyrollManager{
|
|||
};
|
||||
|
||||
private Widevine _widevine = Widevine.Instance;
|
||||
private readonly SemaphoreSlim dubDownloadDelaySemaphore = new(1, 1);
|
||||
private readonly object dubDownloadDelayLock = new();
|
||||
private DateTimeOffset? nextDubDownloadAllowedAtUtc;
|
||||
|
||||
public CrAuth CrAuthEndpoint1;
|
||||
public CrAuth CrAuthEndpoint2;
|
||||
|
|
@ -123,12 +120,11 @@ public class CrunchyrollManager{
|
|||
options.Force = "Y";
|
||||
options.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]";
|
||||
options.Partsize = 10;
|
||||
options.DownloadDelaySeconds = 0;
|
||||
options.DubDownloadDelaySeconds = 0;
|
||||
options.DlSubs = new List<string>{ "en-US" };
|
||||
options.SkipMuxing = false;
|
||||
options.MkvmergeOptions = [];
|
||||
options.FfmpegOptions = [];
|
||||
options.DefaultVideo = "ja-JP";
|
||||
options.DefaultAudio = "ja-JP";
|
||||
options.DefaultSub = "en-US";
|
||||
options.QualityAudio = "best";
|
||||
|
|
@ -384,6 +380,10 @@ public class CrunchyrollManager{
|
|||
|
||||
Parallel.ForEach(historyList, historySeries => {
|
||||
historySeries.Init();
|
||||
|
||||
foreach (var historySeriesSeason in historySeries.Seasons){
|
||||
historySeriesSeason.Init();
|
||||
}
|
||||
});
|
||||
} else{
|
||||
HistoryList = [];
|
||||
|
|
@ -422,7 +422,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Starting",
|
||||
RetryAttemptCount = retryAttemptCount
|
||||
};
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
var res = new DownloadResponse();
|
||||
try{
|
||||
res = await DownloadMediaList(data, options);
|
||||
|
|
@ -454,7 +454,7 @@ public class CrunchyrollManager{
|
|||
DownloadSpeedBytes = 0,
|
||||
Doing = "Download Error" + (!string.IsNullOrEmpty(res.ErrorText) ? " - " + res.ErrorText : ""),
|
||||
};
|
||||
QueueManager.Instance.NotifyQueueItemStateChanged(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
await NotificationPublisher.Instance.PublishDownloadFailedAsync(CrunOptions.NotificationSettings, data, res.ErrorText);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -469,7 +469,7 @@ public class CrunchyrollManager{
|
|||
DownloadSpeedBytes = 0,
|
||||
Doing = "Waiting for Muxing/Encoding"
|
||||
};
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
await QueueManager.Instance.WaitForProcessingSlotAsync(data.Cts.Token);
|
||||
processingSlotHeld = true;
|
||||
}
|
||||
|
|
@ -489,7 +489,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Muxing"
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
if (options.MuxFonts){
|
||||
await FontsManager.Instance.GetFontsAsync();
|
||||
|
|
@ -516,7 +516,6 @@ public class CrunchyrollManager{
|
|||
VideoTitle = res.VideoTitle,
|
||||
Novids = options.Novids,
|
||||
NoCleanup = options.Nocleanup,
|
||||
DefaultVideo = !string.IsNullOrWhiteSpace(options.DefaultVideo) && options.DefaultVideo != "none" ? Languages.FindLang(options.DefaultVideo) : null,
|
||||
DefaultAudio = options.DefaultAudio != "none" ? Languages.FindLang(options.DefaultAudio) : null,
|
||||
DefaultSub = options.DefaultSub != "none" ? Languages.FindLang(options.DefaultSub) : null,
|
||||
MkvmergeOptions = options.MkvmergeOptions,
|
||||
|
|
@ -525,7 +524,6 @@ public class CrunchyrollManager{
|
|||
CcTag = options.CcTag,
|
||||
KeepAllVideos = true,
|
||||
MuxDescription = options.IncludeVideoDescription,
|
||||
ReplaceExistingFiles = options.ReplaceExistingFiles,
|
||||
DlVideoOnce = options.DlVideoOnce,
|
||||
DefaultSubSigns = options.DefaultSubSigns,
|
||||
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
||||
|
|
@ -559,7 +557,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Encoding"
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
|
||||
|
||||
|
|
@ -567,7 +565,7 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
if (options.DownloadToTempFolder){
|
||||
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.Options.Subtitles, options.ReplaceExistingFiles);
|
||||
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.Options.Subtitles);
|
||||
}
|
||||
}
|
||||
} else{
|
||||
|
|
@ -585,7 +583,6 @@ public class CrunchyrollManager{
|
|||
VideoTitle = res.VideoTitle,
|
||||
Novids = options.Novids,
|
||||
NoCleanup = options.Nocleanup,
|
||||
DefaultVideo = !string.IsNullOrWhiteSpace(options.DefaultVideo) && options.DefaultVideo != "none" ? Languages.FindLang(options.DefaultVideo) : null,
|
||||
DefaultAudio = options.DefaultAudio != "none" ? Languages.FindLang(options.DefaultAudio) : null,
|
||||
DefaultSub = options.DefaultSub != "none" ? Languages.FindLang(options.DefaultSub) : null,
|
||||
MkvmergeOptions = options.MkvmergeOptions,
|
||||
|
|
@ -594,7 +591,6 @@ public class CrunchyrollManager{
|
|||
CcTag = options.CcTag,
|
||||
KeepAllVideos = true,
|
||||
MuxDescription = options.IncludeVideoDescription,
|
||||
ReplaceExistingFiles = options.ReplaceExistingFiles,
|
||||
DlVideoOnce = options.DlVideoOnce,
|
||||
DefaultSubSigns = options.DefaultSubSigns,
|
||||
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
||||
|
|
@ -626,7 +622,6 @@ public class CrunchyrollManager{
|
|||
VideoTitle = res.VideoTitle,
|
||||
Novids = options.Novids,
|
||||
NoCleanup = options.Nocleanup,
|
||||
DefaultVideo = !string.IsNullOrWhiteSpace(options.DefaultVideo) && options.DefaultVideo != "none" ? Languages.FindLang(options.DefaultVideo) : null,
|
||||
DefaultAudio = options.DefaultAudio != "none" ? Languages.FindLang(options.DefaultAudio) : null,
|
||||
DefaultSub = options.DefaultSub != "none" ? Languages.FindLang(options.DefaultSub) : null,
|
||||
MkvmergeOptions = options.MkvmergeOptions,
|
||||
|
|
@ -635,7 +630,6 @@ public class CrunchyrollManager{
|
|||
CcTag = options.CcTag,
|
||||
KeepAllVideos = true,
|
||||
MuxDescription = options.IncludeVideoDescription,
|
||||
ReplaceExistingFiles = options.ReplaceExistingFiles,
|
||||
DlVideoOnce = false,
|
||||
DefaultSubSigns = options.DefaultSubSigns,
|
||||
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
|
||||
|
|
@ -670,7 +664,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Encoding"
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
var preset = FfmpegEncoding.GetPreset(options.EncodingPresetName ?? string.Empty);
|
||||
if (preset != null && result.merger != null) await Helpers.RunFFmpegWithPresetAsync(result.merger.Options.Output, preset, data);
|
||||
|
|
@ -693,7 +687,7 @@ public class CrunchyrollManager{
|
|||
})
|
||||
.ToList();
|
||||
|
||||
await MoveFromTempFolder(result.merger, data, options, tempFolder, subtitles, fallbackUsed || options.ReplaceExistingFiles);
|
||||
await MoveFromTempFolder(result.merger, data, options, tempFolder, subtitles, fallbackUsed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -716,7 +710,7 @@ public class CrunchyrollManager{
|
|||
} else{
|
||||
// Move files
|
||||
foreach (var downloadedMedia in res.Data){
|
||||
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options, options.ReplaceExistingFiles);
|
||||
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -742,16 +736,11 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
if (options.History && data.Data is{ Count: > 0 } && (options.HistoryIncludeCrArtists && data.Music || !data.Music)){
|
||||
var ids = data.Data.First().GetOriginalIds();
|
||||
History.SetAsDownloaded(
|
||||
data.SeriesId,
|
||||
ids.seasonID ?? data.SeasonId,
|
||||
ids.guid ?? data.Data.First().MediaId,
|
||||
GetDownloadedDubs(data, res, options),
|
||||
GetDownloadedSoftSubs(res));
|
||||
History.SetAsDownloaded(data.SeriesId, ids.seasonID ?? data.SeasonId, ids.guid ?? data.Data.First().MediaId);
|
||||
}
|
||||
|
||||
if (options.MarkAsWatched && data.Data is{ Count: > 0 }){
|
||||
|
|
@ -774,42 +763,6 @@ public class CrunchyrollManager{
|
|||
return true;
|
||||
}
|
||||
|
||||
private static List<string> GetDownloadedDubs(CrunchyEpMeta data, DownloadResponse? response, CrDownloadOptions? options){
|
||||
if (options?.Noaudio == true){
|
||||
return [];
|
||||
}
|
||||
|
||||
var downloadedFromFiles = NormalizeLocales(response?.Data?
|
||||
.Where(item => item.Type is DownloadMediaType.Video or DownloadMediaType.SyncVideo or DownloadMediaType.Audio)
|
||||
.Select(item => item.Lang.CrLocale));
|
||||
|
||||
if (downloadedFromFiles.Count > 0){
|
||||
return downloadedFromFiles;
|
||||
}
|
||||
|
||||
return NormalizeLocales(data.Data
|
||||
.Where(item => !item.IsAudioRoleDescription)
|
||||
.Select(item => item.Lang?.CrLocale));
|
||||
}
|
||||
|
||||
private static List<string> GetDownloadedSoftSubs(DownloadResponse? response){
|
||||
if (response?.Data == null){
|
||||
return [];
|
||||
}
|
||||
|
||||
return NormalizeLocales(response.Data
|
||||
.Where(item => item.Type == DownloadMediaType.Subtitle && item.Signs != true)
|
||||
.Select(item => item.Language.CrLocale));
|
||||
}
|
||||
|
||||
private static List<string> NormalizeLocales(IEnumerable<string?>? locales){
|
||||
return (locales ?? [])
|
||||
.Where(locale => !string.IsNullOrWhiteSpace(locale))
|
||||
.Select(locale => locale!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#region Temp Files Move
|
||||
|
||||
private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, CrDownloadOptions options, string tempFolderPath, List<SubtitleInput> subtitles, bool replaceExisting = false){
|
||||
|
|
@ -823,7 +776,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Moving Files"
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
if (string.IsNullOrEmpty(tempFolderPath) || !Directory.Exists(tempFolderPath)){
|
||||
Console.WriteLine("Invalid or non-existent temp folder path.");
|
||||
|
|
@ -912,11 +865,7 @@ public class CrunchyrollManager{
|
|||
subsList.Add(subt);
|
||||
}
|
||||
|
||||
var outputPath = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}";
|
||||
if (File.Exists(outputPath) && !string.IsNullOrEmpty(filename)){
|
||||
if (options.ReplaceExistingFiles){
|
||||
Helpers.DeleteFile(outputPath);
|
||||
} else{
|
||||
if (File.Exists($"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}") && !string.IsNullOrEmpty(filename)){
|
||||
string newFilePath = filename;
|
||||
int counter = 1;
|
||||
|
||||
|
|
@ -926,7 +875,6 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
filename = newFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
bool muxDesc = false;
|
||||
|
|
@ -959,7 +907,6 @@ public class CrunchyrollManager{
|
|||
Mkvmerge = options.MkvmergeOptions
|
||||
},
|
||||
Defaults = new Defaults(){
|
||||
Video = options.DefaultVideo,
|
||||
Audio = options.DefaultAudio,
|
||||
Sub = options.DefaultSub
|
||||
},
|
||||
|
|
@ -997,7 +944,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Muxing – Syncing Dub Timings"
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(crunchyEpMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
var basePath = merger.Options.OnlyVid.First().Path;
|
||||
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
|
||||
|
|
@ -1061,7 +1008,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Muxing"
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(crunchyEpMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
}
|
||||
|
||||
if (!options.Mp4 && !muxToMp3){
|
||||
|
|
@ -1090,7 +1037,7 @@ public class CrunchyrollManager{
|
|||
DownloadSpeedBytes = 0,
|
||||
Doing = $"Downloading full-quality fallback video ({string.Join(", ", uniqueFailedLocales)})"
|
||||
};
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
foreach (var syncVideo in res.Data.Where(media => media.Type == DownloadMediaType.SyncVideo && uniqueFailedLocales.Contains(media.Lang.CrLocale, StringComparer.OrdinalIgnoreCase)).ToList()){
|
||||
if (!string.IsNullOrEmpty(syncVideo.Path)){
|
||||
|
|
@ -1199,35 +1146,25 @@ public class CrunchyrollManager{
|
|||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
|
||||
if (!ffmpegAvailable){
|
||||
Console.Error.WriteLine("Missing ffmpeg");
|
||||
MainWindow.Instance.ShowError(
|
||||
"FFmpeg is required to process downloaded video and audio, but CRD could not find it.\n\n" +
|
||||
$"Expected file:\n{CfgManager.PathFFMPEG}\n\n" +
|
||||
"Download FFmpeg and place ffmpeg.exe in CRD's lib folder.",
|
||||
"Download FFmpeg",
|
||||
MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}", "FFmpeg",
|
||||
"https://github.com/GyanD/codexffmpeg/releases/latest");
|
||||
Helpers.EnsureDirectoriesExist(CfgManager.PathFFMPEG);
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "FFmpeg is missing"
|
||||
ErrorText = "Missing ffmpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if (!mkvmergeAvailable){
|
||||
Console.Error.WriteLine("Missing Mkvmerge");
|
||||
MainWindow.Instance.ShowError(
|
||||
"Mkvmerge is required to create the final MKV file, but CRD could not find it.\n\n" +
|
||||
$"Expected file:\n{CfgManager.PathMKVMERGE}\n\n" +
|
||||
"Download MKVToolNix and place mkvmerge.exe in CRD's lib folder.",
|
||||
"Download MKVToolNix",
|
||||
MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}", "Mkvmerge",
|
||||
"https://mkvtoolnix.download/downloads.html#windows");
|
||||
Helpers.EnsureDirectoriesExist(CfgManager.PathMKVMERGE);
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "Mkvmerge is missing"
|
||||
ErrorText = "Missing Mkvmerge"
|
||||
};
|
||||
}
|
||||
} else{
|
||||
|
|
@ -1356,15 +1293,16 @@ public class CrunchyrollManager{
|
|||
|
||||
data.Data = sortedMetaData;
|
||||
|
||||
if (!options.DownloadDelayUseDubBased){
|
||||
await WaitForDownloadDelayAsync(data, options);
|
||||
}
|
||||
|
||||
var epMetaIndex = 0;
|
||||
foreach (CrunchyEpMetaData epMeta in data.Data){
|
||||
if (options.DownloadDelayUseDubBased){
|
||||
await WaitForDownloadDelayAsync(data, options);
|
||||
if (epMetaIndex > 0 && options.DubDownloadDelaySeconds > 0){
|
||||
var delay = TimeSpan.FromSeconds(options.DubDownloadDelaySeconds);
|
||||
data.DownloadProgress.Doing = $"Waiting {options.DubDownloadDelaySeconds}s before next dub";
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
await Task.Delay(delay, data.Cts.Token);
|
||||
}
|
||||
|
||||
epMetaIndex++;
|
||||
Console.WriteLine($"Requesting: [{epMeta.MediaId}] {mediaName}");
|
||||
|
||||
string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId);
|
||||
|
|
@ -1501,7 +1439,7 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(error?.Error)){
|
||||
MainWindow.Instance.ShowError($"Couldn't get Playback Data\n{error.Error}\n{error.Reason}");
|
||||
MainWindow.Instance.ShowError($"Couldn't get Playback Data\n{error.Error}");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
|
|
@ -2092,7 +2030,7 @@ public class CrunchyrollManager{
|
|||
DownloadSpeedBytes = 0,
|
||||
Doing = "Decrypting"
|
||||
};
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
Console.WriteLine("Decryption Needed, attempting to decrypt");
|
||||
|
||||
|
|
@ -2183,7 +2121,7 @@ public class CrunchyrollManager{
|
|||
DownloadSpeedBytes = 0,
|
||||
Doing = "Decrypting video"
|
||||
};
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
var decryptVideo = await Helpers.ExecuteCommandAsyncWorkDir(shaka ? "shaka-packager" : "mp4decrypt", shaka ? CfgManager.PathShakaPackager : CfgManager.PathMP4Decrypt,
|
||||
commandVideo, tempTsFileWorkDir);
|
||||
|
||||
|
|
@ -2244,7 +2182,7 @@ public class CrunchyrollManager{
|
|||
DownloadSpeedBytes = 0,
|
||||
Doing = "Decrypting audio"
|
||||
};
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
var decryptAudio = await Helpers.ExecuteCommandAsyncWorkDir(shaka ? "shaka-packager" : "mp4decrypt", shaka ? CfgManager.PathShakaPackager : CfgManager.PathMP4Decrypt,
|
||||
commandAudio, tempTsFileWorkDir);
|
||||
|
||||
|
|
@ -2397,13 +2335,6 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
// await Task.Delay(options.Waittime);
|
||||
if (options.DownloadDelayUseDubBased){
|
||||
ScheduleNextDownloadDelay(options);
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.DownloadDelayUseDubBased){
|
||||
ScheduleNextDownloadDelay(options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2453,10 +2384,10 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
if (options is{ MuxCover: true, Noaudio: false, Novids: false }){
|
||||
var coverPath = Path.Combine(fileDir, $"{fileName}.cover.png");
|
||||
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(coverPath)){
|
||||
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
|
||||
var bitmap = await Helpers.LoadImage(data.ImageBig);
|
||||
if (bitmap != null){
|
||||
string coverPath = Path.Combine(fileDir, "cover.png");
|
||||
Helpers.EnsureDirectoriesExist(coverPath);
|
||||
await using (var fs = File.OpenWrite(coverPath)){
|
||||
bitmap.Save(fs); // always saves PNG
|
||||
|
|
@ -2469,7 +2400,6 @@ public class CrunchyrollManager{
|
|||
Lang = Languages.DEFAULT_lang,
|
||||
Path = coverPath
|
||||
});
|
||||
data.downloadedFiles.Add(coverPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2496,48 +2426,6 @@ public class CrunchyrollManager{
|
|||
};
|
||||
}
|
||||
|
||||
private async Task WaitForDownloadDelayAsync(CrunchyEpMeta data, CrDownloadOptions options){
|
||||
if (options.DownloadDelaySeconds <= 0){
|
||||
return;
|
||||
}
|
||||
|
||||
await dubDownloadDelaySemaphore.WaitAsync(data.Cts.Token);
|
||||
try{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
DateTimeOffset? nextAllowedAt;
|
||||
lock (dubDownloadDelayLock){
|
||||
nextAllowedAt = nextDubDownloadAllowedAtUtc;
|
||||
}
|
||||
|
||||
if (nextAllowedAt.HasValue && nextAllowedAt.Value > now){
|
||||
var delay = nextAllowedAt.Value - now;
|
||||
var delayTarget = options.DownloadDelayUseDubBased ? "dub" : "episode";
|
||||
data.DownloadProgress.Doing = $"Waiting {Math.Ceiling(delay.TotalSeconds)}s before next {delayTarget}";
|
||||
QueueManager.Instance.RefreshItem(data);
|
||||
await Task.Delay(delay, data.Cts.Token);
|
||||
}
|
||||
|
||||
lock (dubDownloadDelayLock){
|
||||
nextDubDownloadAllowedAtUtc = DateTimeOffset.UtcNow.AddSeconds(options.DownloadDelaySeconds);
|
||||
}
|
||||
} finally{
|
||||
dubDownloadDelaySemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleNextDownloadDelay(CrDownloadOptions options){
|
||||
if (options.DownloadDelaySeconds <= 0){
|
||||
return;
|
||||
}
|
||||
|
||||
var nextAllowedAt = DateTimeOffset.UtcNow.AddSeconds(options.DownloadDelaySeconds);
|
||||
lock (dubDownloadDelayLock){
|
||||
if (!nextDubDownloadAllowedAtUtc.HasValue || nextAllowedAt > nextDubDownloadAllowedAtUtc.Value){
|
||||
nextDubDownloadAllowedAtUtc = nextAllowedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, LanguageItem audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data,
|
||||
DownloadedMedia videoDownloadMedia){
|
||||
if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){
|
||||
|
|
@ -3097,7 +2985,7 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
public async Task DeAuthVideo(string currentMediaId, string videoToken, CrAuth authEndoint){
|
||||
var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/playback/v1/token/{currentMediaId}/{videoToken}/inactive", HttpMethod.Patch, true,
|
||||
var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{videoToken}/inactive", HttpMethod.Patch, true,
|
||||
authEndoint.Token?.access_token, null);
|
||||
var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,10 +120,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
private double? _partSize;
|
||||
|
||||
[ObservableProperty]
|
||||
private double? _downloadDelaySeconds;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _downloadDelayUseDubBased;
|
||||
private double? _dubDownloadDelaySeconds;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _fileName = "";
|
||||
|
|
@ -221,9 +218,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool endpointNotSignedWarning;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedDefaultVideoLang;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem _selectedDefaultDubLang;
|
||||
|
||||
|
|
@ -287,10 +281,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
new(){ Content = "none" },
|
||||
];
|
||||
|
||||
public ObservableCollection<ComboBoxItem> DefaultVideoLangList{ get; } = [
|
||||
new(){ Content = "none" },
|
||||
];
|
||||
|
||||
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = [
|
||||
new(){ Content = "none" },
|
||||
];
|
||||
|
|
@ -379,7 +369,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
|
||||
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
|
||||
DubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
|
||||
DefaultVideoLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
|
||||
DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
|
||||
DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
|
||||
}
|
||||
|
|
@ -400,9 +389,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Hslang) ?? null;
|
||||
SelectedHSLang = hsLang ?? HardSubLangList[0];
|
||||
|
||||
ComboBoxItem? defaultVideoLang = DefaultVideoLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultVideo ?? "")) ?? null;
|
||||
SelectedDefaultVideoLang = defaultVideoLang ?? DefaultVideoLangList[0];
|
||||
|
||||
ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null;
|
||||
SelectedDefaultDubLang = defaultDubLang ?? DefaultDubLangList[0];
|
||||
|
||||
|
|
@ -491,8 +477,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay;
|
||||
DefaultSubSigns = options.DefaultSubSigns;
|
||||
PartSize = options.Partsize;
|
||||
DownloadDelaySeconds = options.DownloadDelaySeconds;
|
||||
DownloadDelayUseDubBased = options.DownloadDelayUseDubBased;
|
||||
DubDownloadDelaySeconds = options.DubDownloadDelaySeconds;
|
||||
IncludeEpisodeDescription = options.IncludeVideoDescription;
|
||||
FileTitle = options.VideoTitle ?? "";
|
||||
IncludeSignSubs = options.IncludeSignsSubs;
|
||||
|
|
@ -594,8 +579,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
|
||||
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs;
|
||||
CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000);
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadDelaySeconds = Math.Max((int)(DownloadDelaySeconds ?? 0), 0);
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadDelayUseDubBased = DownloadDelayUseDubBased;
|
||||
CrunchyrollManager.Instance.CrunOptions.DubDownloadDelaySeconds = Math.Max((int)(DubDownloadDelaySeconds ?? 0), 0);
|
||||
CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic = SearchFetchFeaturedMusic;
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
|
||||
|
|
@ -616,7 +600,6 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
|
||||
CrunchyrollManager.Instance.CrunOptions.HsRawFallback = HsRawFallback;
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.DefaultVideo = SelectedDefaultVideoLang.Content + "";
|
||||
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
|
||||
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
|
||||
|
||||
|
|
@ -759,6 +742,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){
|
||||
historySeries.Init();
|
||||
foreach (var historySeriesSeason in historySeries.Seasons){
|
||||
historySeriesSeason.Init();
|
||||
}
|
||||
}
|
||||
} else{
|
||||
CrunchyrollManager.Instance.HistoryList = [];
|
||||
|
|
|
|||
|
|
@ -255,23 +255,16 @@
|
|||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Download Delay"
|
||||
Description="Delay in seconds before starting the next episode download. 0 disables the delay.">
|
||||
<controls:SettingsExpanderItem Content="Dub Download Delay"
|
||||
Description="Delay in seconds before starting the next selected dub. 0 disables the delay.">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<controls:NumberBox Minimum="0"
|
||||
Value="{Binding DownloadDelaySeconds}"
|
||||
Value="{Binding DubDownloadDelaySeconds}"
|
||||
SpinButtonPlacementMode="Hidden"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Dub Based Download Delay"
|
||||
Description="Apply the download delay before every selected dub instead of only between episodes.">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding DownloadDelayUseDubBased}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Stream Endpoint ">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
|
|
@ -567,15 +560,6 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Video ">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||
ItemsSource="{Binding DefaultVideoLangList}"
|
||||
SelectedItem="{Binding SelectedDefaultVideoLang}">
|
||||
</ComboBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Audio ">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ public class History{
|
|||
|
||||
if (seasonData.Data is{ Count: > 0 }){
|
||||
result = true;
|
||||
await crunInstance.History.UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>(), false);
|
||||
await crunInstance.History.UpdateWithSeasonData(seasonData.Data.ToList<IHistorySource>());
|
||||
break;
|
||||
}
|
||||
} catch{
|
||||
|
|
@ -130,11 +130,11 @@ public class History{
|
|||
var musicVideoGroups = episodeList.Where(e => e.EpisodeType == EpisodeType.MusicVideo).GroupBy(e => e.Artist.Id);
|
||||
|
||||
foreach (var concertGroup in concertGroups){
|
||||
await UpdateWithSeasonData(concertGroup.ToList<IHistorySource>(), false);
|
||||
await UpdateWithSeasonData(concertGroup.ToList<IHistorySource>());
|
||||
}
|
||||
|
||||
foreach (var musicVideoGroup in musicVideoGroups){
|
||||
await UpdateWithSeasonData(musicVideoGroup.ToList<IHistorySource>(), false);
|
||||
await UpdateWithSeasonData(musicVideoGroup.ToList<IHistorySource>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -203,6 +203,13 @@ public class History{
|
|||
var originalItems = seriesGroup.Where(IsOriginalItem).ToList();
|
||||
|
||||
if (originalItems.Count > 0){
|
||||
if (allOriginalsInHistory){
|
||||
var sT = seriesGroup.Select(e => e.EpisodeMetadata?.SeriesTitle)
|
||||
.FirstOrDefault(t => !string.IsNullOrWhiteSpace(t)) ?? "";
|
||||
// Console.WriteLine($"[INFO] Skipping SeriesId={seriesId} {sT} - all ORIGINAL episodes already in history.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var convertedList = originalItems.Select(crBrowseEpisode => crBrowseEpisode.ToCrunchyEpisode()).ToList();
|
||||
|
||||
await crunInstance.History.UpdateWithSeasonData(convertedList.ToList<IHistorySource>());
|
||||
|
|
@ -215,7 +222,6 @@ public class History{
|
|||
|
||||
if (allOriginalsInHistory){
|
||||
// Console.WriteLine($"[INFO] Skipping SeriesId={seriesId} - originals implied by Versions already in history.");
|
||||
RefreshExistingEpisodesFromBrowse(seriesGroup);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -223,8 +229,6 @@ public class History{
|
|||
|
||||
if (HasAllSeriesEpisodesInHistory(historyIndex, seriesId, seriesGroup)){
|
||||
Console.WriteLine($"[History] Skip (already in history): {seriesId}");
|
||||
var convertedList = seriesGroup.Select(crBrowseEpisode => crBrowseEpisode.ToCrunchyEpisode()).ToList();
|
||||
await crunInstance.History.UpdateWithSeasonData(convertedList.ToList<IHistorySource>());
|
||||
} else{
|
||||
await CrUpdateSeries(seriesId, null);
|
||||
Console.WriteLine($"[History] Updating (full series): {seriesId}");
|
||||
|
|
@ -234,37 +238,6 @@ public class History{
|
|||
return;
|
||||
}
|
||||
|
||||
private void RefreshExistingEpisodesFromBrowse(IEnumerable<CrBrowseEpisode> episodes){
|
||||
var refreshedSeriesIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var episode in episodes){
|
||||
var seriesId = episode.EpisodeMetadata?.SeriesId;
|
||||
var seasonId = TryGetOriginalSeasonId(episode);
|
||||
var episodeId = TryGetOriginalId(episode) ?? episode.Id;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(seriesId) ||
|
||||
string.IsNullOrWhiteSpace(seasonId) ||
|
||||
string.IsNullOrWhiteSpace(episodeId)){
|
||||
continue;
|
||||
}
|
||||
|
||||
var historyEpisode = GetHistoryEpisode(seriesId, seasonId, episodeId);
|
||||
|
||||
if (historyEpisode == null){
|
||||
continue;
|
||||
}
|
||||
|
||||
var historySource = episode.ToCrunchyEpisode();
|
||||
historyEpisode.UpdateAvailableMedia(historySource.GetEpisodeAvailableDubLang(), historySource.GetEpisodeAvailableSoftSubs());
|
||||
historyEpisode.IsEpisodeAvailableOnStreamingService = true;
|
||||
refreshedSeriesIds.Add(seriesId);
|
||||
}
|
||||
|
||||
foreach (var seriesId in refreshedSeriesIds){
|
||||
crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId)?.UpdateNewEpisodes();
|
||||
}
|
||||
}
|
||||
|
||||
private string? TryGetOriginalId(CrBrowseEpisode e) =>
|
||||
e.EpisodeMetadata?.versions?
|
||||
.FirstOrDefault(v => v.Original && !string.IsNullOrWhiteSpace(v.Guid))
|
||||
|
|
@ -311,7 +284,7 @@ public class History{
|
|||
/// <summary>
|
||||
/// This method updates the History with a list of episodes. The episodes have to be from the same season.
|
||||
/// </summary>
|
||||
private async Task UpdateWithSeasonData(List<IHistorySource> episodeList, bool matchSonarr = true){
|
||||
private async Task UpdateWithSeasonData(List<IHistorySource> episodeList){
|
||||
if (episodeList is{ Count: > 0 }){
|
||||
var firstEpisode = episodeList.First();
|
||||
var seriesId = firstEpisode.GetSeriesId();
|
||||
|
|
@ -344,7 +317,6 @@ public class History{
|
|||
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
|
||||
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
|
||||
EpisodeType = historySource.GetEpisodeType(),
|
||||
EpisodeSeriesType = historySource.GetSeriesType(),
|
||||
IsEpisodeAvailableOnStreamingService = true,
|
||||
ThumbnailImageUrl = historySource.GetImageUrl(),
|
||||
};
|
||||
|
|
@ -364,7 +336,8 @@ public class History{
|
|||
historyEpisode.IsEpisodeAvailableOnStreamingService = true;
|
||||
historyEpisode.ThumbnailImageUrl = historySource.GetImageUrl();
|
||||
|
||||
historyEpisode.UpdateAvailableMedia(historySource.GetEpisodeAvailableDubLang(), historySource.GetEpisodeAvailableSoftSubs());
|
||||
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
|
||||
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +348,6 @@ public class History{
|
|||
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
|
||||
|
||||
historySeries.Seasons.Add(newSeason);
|
||||
newSeason.StreamingService = historySeries.SeriesStreamingService;
|
||||
newSeason.Init();
|
||||
}
|
||||
|
||||
|
|
@ -404,79 +376,36 @@ public class History{
|
|||
_ = historySeries.LoadImage();
|
||||
historySeries.UpdateNewEpisodes();
|
||||
historySeries.Init();
|
||||
newSeason.Init();
|
||||
}
|
||||
|
||||
SortItems();
|
||||
if (historySeries != null){
|
||||
SortSeasons(historySeries);
|
||||
if (matchSonarr){
|
||||
await MatchHistorySeriesWithSonarr(historySeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MatchHistorySeriesWithSonarr(HistorySeries historySeries){
|
||||
if (crunInstance.CrunOptions.SonarrProperties is not{ SonarrEnabled: true } ||
|
||||
historySeries.SeriesType == SeriesType.Artist){
|
||||
return;
|
||||
}
|
||||
|
||||
MatchSingleHistorySeriesWithSonarr(historySeries);
|
||||
await MatchHistoryEpisodesWithSonarr(false, historySeries);
|
||||
}
|
||||
|
||||
private void MatchSingleHistorySeriesWithSonarr(HistorySeries historySeries){
|
||||
if (string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
|
||||
if (sonarrSeries != null){
|
||||
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
|
||||
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
|
||||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
}
|
||||
} else{
|
||||
var sonarrSeries = SonarrClient.Instance.SonarrSeries.FirstOrDefault(series => series.Id + "" == historySeries.SonarrSeriesId);
|
||||
if (sonarrSeries != null){
|
||||
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
|
||||
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
|
||||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
} else{
|
||||
Console.Error.WriteLine($"Unable to find sonarr series for {historySeries.SeriesTitle}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId, IEnumerable<string?>? downloadedDubs = null, IEnumerable<string?>? downloadedSoftSubs = null){
|
||||
public void SetAsDownloaded(string? seriesId, string? seasonId, string episodeId){
|
||||
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
|
||||
|
||||
if (historySeries == null){
|
||||
return;
|
||||
}
|
||||
if (historySeries != null){
|
||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||
|
||||
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
|
||||
if (historySeason != null){
|
||||
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
|
||||
|
||||
if (historySeason != null){
|
||||
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
|
||||
|
||||
if (historyEpisode != null){
|
||||
historyEpisode.SetDownloadedMedia(NormalizeLocales(downloadedDubs), NormalizeLocales(downloadedSoftSubs));
|
||||
|
||||
historySeason.UpdateDownloaded();
|
||||
return;
|
||||
if (historyEpisode != null){
|
||||
historyEpisode.WasDownloaded = true;
|
||||
historySeason.UpdateDownloaded();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't update download History", ToastType.Warning, 2));
|
||||
}
|
||||
|
||||
private static List<string> NormalizeLocales(IEnumerable<string?>? locales){
|
||||
return (locales ?? [])
|
||||
.Where(locale => !string.IsNullOrWhiteSpace(locale))
|
||||
.Select(locale => locale!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public HistoryEpisode? GetHistoryEpisode(string? seriesId, string? seasonId, string episodeId){
|
||||
return CrunchyrollManager.Instance.HistoryList
|
||||
.FirstOrDefault(series => series.SeriesId == seriesId)?
|
||||
|
|
@ -867,14 +796,10 @@ public class History{
|
|||
}
|
||||
|
||||
public void MatchHistorySeriesWithSonarr(bool updateAll){
|
||||
if (crunInstance.CrunOptions.SonarrProperties is not{ SonarrEnabled: true }){
|
||||
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
|
||||
return;
|
||||
}
|
||||
|
||||
var sonarrSeriesById = updateAll
|
||||
? SonarrClient.Instance.SonarrSeries.ToDictionary(series => series.Id.ToString())
|
||||
: [];
|
||||
|
||||
foreach (var historySeries in crunInstance.HistoryList){
|
||||
if (string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
var sonarrSeries = FindClosestMatch(historySeries.SeriesTitle ?? string.Empty);
|
||||
|
|
@ -884,7 +809,8 @@ public class History{
|
|||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
}
|
||||
} else if (updateAll){
|
||||
if (sonarrSeriesById.TryGetValue(historySeries.SonarrSeriesId, out var sonarrSeries)){
|
||||
var sonarrSeries = SonarrClient.Instance.SonarrSeries.FirstOrDefault(series => series.Id + "" == historySeries.SonarrSeriesId);
|
||||
if (sonarrSeries != null){
|
||||
historySeries.SonarrSeriesId = sonarrSeries.Id + "";
|
||||
historySeries.SonarrTvDbId = sonarrSeries.TvdbId + "";
|
||||
historySeries.SonarrSlugTitle = sonarrSeries.TitleSlug;
|
||||
|
|
@ -895,130 +821,120 @@ public class History{
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
public async Task MatchHistoryEpisodesWithSonarr(bool rematchAll, HistorySeries historySeries){
|
||||
if (crunInstance.CrunOptions.SonarrProperties is not{ SonarrEnabled: true }){
|
||||
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(historySeries.SonarrSeriesId, out var sonarrSeriesId)){
|
||||
return;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
List<SonarrEpisode> episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
|
||||
|
||||
List<SonarrEpisode> episodes = await SonarrClient.Instance.GetEpisodes(sonarrSeriesId);
|
||||
historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
|
||||
|
||||
historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
|
||||
List<HistoryEpisode> allHistoryEpisodes = [];
|
||||
|
||||
var allHistoryEpisodes = historySeries.Seasons
|
||||
.SelectMany(historySeriesSeason => historySeriesSeason.EpisodesList)
|
||||
.ToList();
|
||||
|
||||
var episodesById = episodes.ToDictionary(episode => episode.Id);
|
||||
var usedSonarrEpisodeIds = new HashSet<int>();
|
||||
var episodesToMatch = new List<HistoryEpisode>();
|
||||
|
||||
if (!rematchAll){
|
||||
foreach (var historyEpisode in allHistoryEpisodes){
|
||||
if (int.TryParse(historyEpisode.SonarrEpisodeId, out var sonarrEpisodeId) &&
|
||||
episodesById.TryGetValue(sonarrEpisodeId, out var sonarrEpisode) &&
|
||||
usedSonarrEpisodeIds.Add(sonarrEpisode.Id)){
|
||||
historyEpisode.AssignSonarrEpisodeData(sonarrEpisode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId)){
|
||||
historyEpisode.ClearSonarrEpisodeData();
|
||||
}
|
||||
|
||||
episodesToMatch.Add(historyEpisode);
|
||||
foreach (var historySeriesSeason in historySeries.Seasons){
|
||||
allHistoryEpisodes.AddRange(historySeriesSeason.EpisodesList);
|
||||
}
|
||||
} else{
|
||||
foreach (var historyEpisode in allHistoryEpisodes){
|
||||
historyEpisode.ClearSonarrEpisodeData();
|
||||
episodesToMatch.Add(historyEpisode);
|
||||
|
||||
if (!rematchAll){
|
||||
var historyEpisodesWithSonarrIds = allHistoryEpisodes
|
||||
.Where(e => !string.IsNullOrEmpty(e.SonarrEpisodeId))
|
||||
.ToList();
|
||||
|
||||
Parallel.ForEach(historyEpisodesWithSonarrIds, historyEpisode => {
|
||||
var sonarrEpisode = episodes.FirstOrDefault(e => e.Id.ToString().Equals(historyEpisode.SonarrEpisodeId));
|
||||
|
||||
if (sonarrEpisode != null){
|
||||
historyEpisode.AssignSonarrEpisodeData(sonarrEpisode);
|
||||
}
|
||||
});
|
||||
|
||||
var historyEpisodeIds = new HashSet<string>(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!));
|
||||
|
||||
episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString()));
|
||||
|
||||
allHistoryEpisodes = allHistoryEpisodes
|
||||
.Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var titleAvailableEpisodes = episodes
|
||||
.Where(episode => !usedSonarrEpisodeIds.Contains(episode.Id))
|
||||
.ToList();
|
||||
List<HistoryEpisode> failedEpisodes = [];
|
||||
|
||||
var titleCandidates = episodesToMatch
|
||||
.AsParallel()
|
||||
.Select(historyEpisode => {
|
||||
var match = FindClosestMatchEpisodeWithScore(titleAvailableEpisodes, historyEpisode.EpisodeTitle ?? string.Empty);
|
||||
Parallel.ForEach(allHistoryEpisodes, historyEpisode => {
|
||||
if (string.IsNullOrEmpty(historyEpisode.SonarrEpisodeId) || rematchAll){
|
||||
// Create a copy of the episodes list for each thread
|
||||
var episodesCopy = new List<SonarrEpisode>(episodes);
|
||||
|
||||
return new{
|
||||
HistoryEpisode = historyEpisode,
|
||||
match.Episode,
|
||||
match.Score
|
||||
};
|
||||
})
|
||||
.Where(candidate => candidate.Episode != null)
|
||||
.OrderByDescending(candidate => candidate.Score)
|
||||
.ToList();
|
||||
|
||||
var failedEpisodes = new List<HistoryEpisode>();
|
||||
var matchedHistoryEpisodes = new HashSet<HistoryEpisode>();
|
||||
|
||||
foreach (var candidate in titleCandidates){
|
||||
if (TryAssignSonarrEpisode(candidate.HistoryEpisode, candidate.Episode, usedSonarrEpisodeIds)){
|
||||
matchedHistoryEpisodes.Add(candidate.HistoryEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
failedEpisodes.AddRange(episodesToMatch.Where(historyEpisode => !matchedHistoryEpisodes.Contains(historyEpisode)));
|
||||
|
||||
foreach (var historyEpisode in failedEpisodes.ToList()){
|
||||
var episode = episodes.FirstOrDefault(ele => {
|
||||
if (usedSonarrEpisodeIds.Contains(ele.Id)){
|
||||
return false;
|
||||
var episode = FindClosestMatchEpisodes(episodesCopy, historyEpisode.EpisodeTitle ?? string.Empty);
|
||||
if (episode != null){
|
||||
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||
lock (_lock){
|
||||
episodes.Remove(episode);
|
||||
}
|
||||
} else{
|
||||
lock (_lock){
|
||||
failedEpisodes.Add(historyEpisode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var episodeNumberStr = ele.EpisodeNumber.ToString();
|
||||
var seasonNumberStr = ele.SeasonNumber.ToString();
|
||||
|
||||
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
|
||||
});
|
||||
|
||||
if (TryAssignSonarrEpisode(historyEpisode, episode, usedSonarrEpisodeIds)){
|
||||
failedEpisodes.Remove(historyEpisode);
|
||||
}
|
||||
Parallel.ForEach(failedEpisodes, historyEpisode => {
|
||||
var episode = episodes.Find(ele => {
|
||||
if (ele == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
var episodeNumberStr = ele.EpisodeNumber.ToString();
|
||||
var seasonNumberStr = ele.SeasonNumber.ToString();
|
||||
|
||||
return episodeNumberStr == historyEpisode.Episode && seasonNumberStr == historyEpisode.EpisodeSeasonNum;
|
||||
});
|
||||
if (episode != null){
|
||||
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||
|
||||
lock (_lock){
|
||||
episodes.Remove(episode);
|
||||
}
|
||||
} else{
|
||||
var episode1 = episodes.Find(ele => {
|
||||
if (ele == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(historyEpisode.EpisodeDescription) && !string.IsNullOrEmpty(ele.Overview) && Helpers.CalculateCosineSimilarity(ele.Overview, historyEpisode.EpisodeDescription) > 0.8;
|
||||
});
|
||||
|
||||
if (episode1 != null){
|
||||
historyEpisode.AssignSonarrEpisodeData(episode1);
|
||||
|
||||
lock (_lock){
|
||||
episodes.Remove(episode1);
|
||||
}
|
||||
} else{
|
||||
var episode2 = episodes.Find(ele => {
|
||||
if (ele == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
return ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode;
|
||||
});
|
||||
if (episode2 != null){
|
||||
historyEpisode.AssignSonarrEpisodeData(episode2);
|
||||
|
||||
lock (_lock){
|
||||
episodes.Remove(episode2);
|
||||
}
|
||||
} else{
|
||||
Console.Error.WriteLine($"Could not match episode {historyEpisode.EpisodeTitle} to sonarr episode");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var historyEpisode in failedEpisodes.ToList()){
|
||||
var episode = episodes.FirstOrDefault(ele =>
|
||||
!usedSonarrEpisodeIds.Contains(ele.Id) &&
|
||||
!string.IsNullOrEmpty(historyEpisode.EpisodeDescription) &&
|
||||
!string.IsNullOrEmpty(ele.Overview) &&
|
||||
Helpers.CalculateCosineSimilarity(ele.Overview, historyEpisode.EpisodeDescription) > 0.8);
|
||||
|
||||
if (TryAssignSonarrEpisode(historyEpisode, episode, usedSonarrEpisodeIds)){
|
||||
failedEpisodes.Remove(historyEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var historyEpisode in failedEpisodes.ToList()){
|
||||
var episode = episodes.FirstOrDefault(ele =>
|
||||
!usedSonarrEpisodeIds.Contains(ele.Id) &&
|
||||
ele.AbsoluteEpisodeNumber + "" == historyEpisode.Episode);
|
||||
|
||||
if (TryAssignSonarrEpisode(historyEpisode, episode, usedSonarrEpisodeIds)){
|
||||
failedEpisodes.Remove(historyEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var historyEpisode in failedEpisodes){
|
||||
historyEpisode.ClearSonarrEpisodeData();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryAssignSonarrEpisode(HistoryEpisode historyEpisode, SonarrEpisode? episode, HashSet<int> usedSonarrEpisodeIds){
|
||||
if (episode == null || !usedSonarrEpisodeIds.Add(episode.Id)){
|
||||
return false;
|
||||
}
|
||||
|
||||
historyEpisode.AssignSonarrEpisodeData(episode);
|
||||
return true;
|
||||
}
|
||||
|
||||
public string GetNextAirDate(List<SonarrEpisode> episodes){
|
||||
|
|
@ -1064,27 +980,24 @@ public class History{
|
|||
}
|
||||
|
||||
public SonarrEpisode? FindClosestMatchEpisodes(List<SonarrEpisode> episodeList, string title){
|
||||
return FindClosestMatchEpisodeWithScore(episodeList, title).Episode;
|
||||
}
|
||||
|
||||
private (SonarrEpisode? Episode, double Score) FindClosestMatchEpisodeWithScore(List<SonarrEpisode> episodeList, string title){
|
||||
if (string.IsNullOrWhiteSpace(title) || episodeList.Count == 0){
|
||||
return (null, 0.0);
|
||||
}
|
||||
|
||||
SonarrEpisode? closestMatch = null;
|
||||
double highestSimilarity = 0.0;
|
||||
foreach (var episode in episodeList){
|
||||
if (!string.IsNullOrWhiteSpace(episode.Title)){
|
||||
object lockObject = new object(); // To synchronize access to shared variables
|
||||
|
||||
Parallel.ForEach(episodeList, episode => {
|
||||
if (episode != null){
|
||||
double similarity = CalculateSimilarity(episode.Title, title);
|
||||
if (similarity <= highestSimilarity) continue;
|
||||
|
||||
highestSimilarity = similarity;
|
||||
closestMatch = episode;
|
||||
lock (lockObject) // Ensure thread-safe access to shared variables
|
||||
{
|
||||
if (similarity > highestSimilarity){
|
||||
highestSimilarity = similarity;
|
||||
closestMatch = episode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return highestSimilarity < 0.8 ? (null, highestSimilarity) : (closestMatch, highestSimilarity);
|
||||
return highestSimilarity < 0.8 ? null : closestMatch;
|
||||
}
|
||||
|
||||
public CrBrowseSeries? FindClosestMatchCrSeries(List<CrBrowseSeries> episodeList, string title){
|
||||
|
|
@ -1162,32 +1075,7 @@ public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
|
|||
return xDouble.CompareTo(yDouble);
|
||||
}
|
||||
|
||||
if (TryParseEpisodeSortNumber(x?.Episode, out xDouble) &&
|
||||
TryParseEpisodeSortNumber(y?.Episode, out yDouble)){
|
||||
int numericCompare = xDouble.CompareTo(yDouble);
|
||||
if (numericCompare != 0){
|
||||
return numericCompare;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to string comparison if not parseable as doubles
|
||||
return string.Compare(x?.Episode, y?.Episode, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool TryParseEpisodeSortNumber(string? episode, out double episodeNumber){
|
||||
return double.TryParse(episode, NumberStyles.Any, CultureInfo.InvariantCulture, out episodeNumber) ||
|
||||
TryParseEpisodeRangeStart(episode, out episodeNumber);
|
||||
}
|
||||
|
||||
private static bool TryParseEpisodeRangeStart(string? episode, out double episodeNumber){
|
||||
episodeNumber = 0;
|
||||
if (string.IsNullOrWhiteSpace(episode)){
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] parts = episode.Split('-', 2, StringSplitOptions.TrimEntries);
|
||||
return parts.Length == 2 &&
|
||||
double.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out episodeNumber) &&
|
||||
double.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out _);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public sealed partial class ProgramManager : ObservableObject{
|
|||
private bool fetchingData;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool updateAvailable;
|
||||
private bool updateAvailable = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool finishedLoading;
|
||||
|
|
@ -206,7 +206,7 @@ public sealed partial class ProgramManager : ObservableObject{
|
|||
internal async Task RefreshHistoryWithNewReleases(CrunchyrollManager crunchyManager, CrDownloadOptions crunOptions){
|
||||
var newEpisodesBase = await crunchyManager.CrEpisode.GetNewEpisodes(
|
||||
string.IsNullOrEmpty(crunOptions.HistoryLang) ? crunchyManager.DefaultLocale : crunOptions.HistoryLang,
|
||||
1000, null, true);
|
||||
2000, null, true);
|
||||
var newEpisodes = newEpisodesBase?.Data ?? [];
|
||||
|
||||
if (newEpisodesBase is{ Data.Count: > 0 }){
|
||||
|
|
@ -244,7 +244,7 @@ public sealed partial class ProgramManager : ObservableObject{
|
|||
|
||||
releaseFeedEpisodes ??= (await crunchyManager.CrEpisode.GetNewEpisodes(
|
||||
string.IsNullOrEmpty(crunchyManager.CrunOptions.HistoryLang) ? crunchyManager.DefaultLocale : crunchyManager.CrunOptions.HistoryLang,
|
||||
1000, null, true))?.Data ?? [];
|
||||
2000, null, true))?.Data ?? [];
|
||||
|
||||
var notificationSettings = settings.NotificationSettings;
|
||||
var historyUpdated = false;
|
||||
|
|
@ -338,6 +338,9 @@ public sealed partial class ProgramManager : ObservableObject{
|
|||
try{
|
||||
CrunchyrollManager.Instance.InitOptions();
|
||||
|
||||
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
|
||||
await Updater.Instance.CheckGhJsonAsync();
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
|
||||
if (_faTheme != null) _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
|
||||
}
|
||||
|
|
@ -353,17 +356,10 @@ public sealed partial class ProgramManager : ObservableObject{
|
|||
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
|
||||
}
|
||||
}
|
||||
|
||||
await Updater.Instance.CheckGhJsonAsync();
|
||||
|
||||
|
||||
await CrunchyrollManager.Instance.Init();
|
||||
|
||||
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
|
||||
|
||||
|
||||
FinishedLoading = true;
|
||||
QueueManager.Instance.UpdateDownloadListItems();
|
||||
|
||||
|
||||
|
||||
await WorkOffArgsTasks();
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
private bool hasFailedItem;
|
||||
|
||||
public event EventHandler? QueueStateChanged;
|
||||
public event EventHandler? QueuePersistenceRequested;
|
||||
|
||||
private readonly CrunchyrollManager crunchyrollManager;
|
||||
|
||||
|
|
@ -72,10 +71,7 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
crunchyrollManager.CrunOptions.SimultaneousProcessingJobs);
|
||||
|
||||
queue.CollectionChanged += UpdateItemListOnRemove;
|
||||
queue.CollectionChanged += (_, _) => {
|
||||
OnQueueStateChanged();
|
||||
OnQueuePersistenceRequested();
|
||||
};
|
||||
queue.CollectionChanged += (_, _) => OnQueueStateChanged();
|
||||
}
|
||||
|
||||
public void AddToQueue(CrunchyEpMeta item){
|
||||
|
|
@ -101,27 +97,6 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
uiMutationQueue.Enqueue(() => queue.Refresh());
|
||||
}
|
||||
|
||||
public void RefreshItem(CrunchyEpMeta item){
|
||||
uiMutationQueue.Enqueue(() => RefreshItemCore(item));
|
||||
}
|
||||
|
||||
public void NotifyQueueItemStateChanged(CrunchyEpMeta item){
|
||||
uiMutationQueue.Enqueue(() => {
|
||||
RefreshItemCore(item);
|
||||
HasFailedItem = queue.Any(queueItem => queueItem.DownloadProgress.IsError);
|
||||
OnQueueStateChanged();
|
||||
OnQueuePersistenceRequested();
|
||||
|
||||
if (crunchyrollManager.CrunOptions.AutoDownload){
|
||||
RequestPump();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void RefreshItemCore(CrunchyEpMeta item){
|
||||
downloadItems.Find(item)?.Refresh();
|
||||
}
|
||||
|
||||
|
||||
public bool TryStartDownload(DownloadItemModel model){
|
||||
var item = model.epMeta;
|
||||
|
|
@ -235,13 +210,7 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
}
|
||||
|
||||
private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){
|
||||
if (e.Action == NotifyCollectionChangedAction.Add){
|
||||
if (e.NewItems != null){
|
||||
foreach (var newItem in e.NewItems.OfType<CrunchyEpMeta>()){
|
||||
downloadItems.AddOrRefresh(newItem);
|
||||
}
|
||||
}
|
||||
} else if (e.Action == NotifyCollectionChangedAction.Remove){
|
||||
if (e.Action == NotifyCollectionChangedAction.Remove){
|
||||
if (e.OldItems != null){
|
||||
foreach (var oldItem in e.OldItems.OfType<CrunchyEpMeta>()){
|
||||
downloadItems.Remove(oldItem);
|
||||
|
|
@ -249,15 +218,9 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
}
|
||||
} else if (e.Action == NotifyCollectionChangedAction.Reset && queue.Count == 0){
|
||||
downloadItems.Clear();
|
||||
} else{
|
||||
UpdateDownloadListItems();
|
||||
}
|
||||
|
||||
HasFailedItem = queue.Any(item => item.DownloadProgress.IsError);
|
||||
|
||||
if (crunchyrollManager.CrunOptions.AutoDownload){
|
||||
RequestPump();
|
||||
}
|
||||
UpdateDownloadListItems();
|
||||
}
|
||||
|
||||
public void MarkDownloadFinished(CrunchyEpMeta item, bool removeFromQueue){
|
||||
|
|
@ -267,12 +230,10 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
if (index >= 0)
|
||||
queue.RemoveAt(index);
|
||||
} else{
|
||||
RefreshItemCore(item);
|
||||
HasFailedItem = queue.Any(queueItem => queueItem.DownloadProgress.IsError);
|
||||
queue.Refresh();
|
||||
}
|
||||
|
||||
OnQueueStateChanged();
|
||||
OnQueuePersistenceRequested();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -318,9 +279,6 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
if (!crunchyrollManager.CrunOptions.AutoDownload)
|
||||
return;
|
||||
|
||||
if (!ProgramManager.Instance.FinishedLoading)
|
||||
return;
|
||||
|
||||
lock (autoDownloadBlockLock){
|
||||
if (autoDownloadBlockedUntilUtc.HasValue && !HasPendingRetryItems()){
|
||||
autoDownloadBlockedUntilUtc = null;
|
||||
|
|
@ -424,9 +382,8 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
}
|
||||
}
|
||||
|
||||
if (crunchyrollManager.CrunOptions.AutoDownload){
|
||||
RequestPump();
|
||||
}
|
||||
RefreshQueue();
|
||||
UpdateDownloadListItems();
|
||||
} catch (OperationCanceledException){
|
||||
// ignored
|
||||
}
|
||||
|
|
@ -435,7 +392,8 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
|
||||
public void ScheduleRetry(CrunchyEpMeta item, TimeSpan delay, string statusText, CancellationToken cancellationToken = default){
|
||||
item.DownloadProgress.ScheduleRetry(delay, statusText);
|
||||
NotifyQueueItemStateChanged(item);
|
||||
RefreshQueue();
|
||||
OnQueueStateChanged();
|
||||
|
||||
ScheduleRetryWake(item, item.DownloadProgress.RetryAtUtc, cancellationToken);
|
||||
}
|
||||
|
|
@ -488,7 +446,8 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
}
|
||||
|
||||
item.DownloadProgress.RetryAtUtc = null;
|
||||
NotifyQueueItemStateChanged(item);
|
||||
RefreshQueue();
|
||||
UpdateDownloadListItems();
|
||||
} catch (OperationCanceledException){
|
||||
// ignored
|
||||
}
|
||||
|
|
@ -499,10 +458,6 @@ public sealed partial class QueueManager : ObservableObject{
|
|||
private void OnQueueStateChanged(){
|
||||
QueueStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void OnQueuePersistenceRequested(){
|
||||
QueuePersistenceRequested?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void NotifyDownloadStateChanged(){
|
||||
OnPropertyChanged(nameof(ActiveDownloads));
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using System.Reflection;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
@ -286,39 +285,26 @@ public class CfgManager{
|
|||
}
|
||||
|
||||
|
||||
private static object fileLock = new object();
|
||||
|
||||
public static void WriteJsonToFile(string pathToFile, object obj){
|
||||
string? directoryPath = Path.GetDirectoryName(pathToFile);
|
||||
if (string.IsNullOrEmpty(directoryPath))
|
||||
directoryPath = Environment.CurrentDirectory;
|
||||
try{
|
||||
// Check if the directory exists; if not, create it.
|
||||
string directoryPath = Path.GetDirectoryName(pathToFile);
|
||||
if (!Directory.Exists(directoryPath)){
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
string key = Path.GetFullPath(pathToFile);
|
||||
object gate = _pathLocks.GetOrAdd(key, _ => new object());
|
||||
|
||||
lock (gate){
|
||||
string tmp = Path.Combine(
|
||||
directoryPath,
|
||||
"." + Path.GetFileName(pathToFile) + "." + Guid.NewGuid().ToString("N") + ".tmp");
|
||||
|
||||
try{
|
||||
using (var fileStream = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
lock (fileLock){
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var streamWriter = new StreamWriter(fileStream))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
|
||||
var serializer = new JsonSerializer();
|
||||
serializer.Serialize(jsonWriter, obj);
|
||||
}
|
||||
|
||||
ReplaceFileWithRetry(tmp, pathToFile);
|
||||
} catch (Exception ex){
|
||||
try{
|
||||
if (File.Exists(tmp)) File.Delete(tmp);
|
||||
} catch{
|
||||
// ignored
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"An error occurred writing {pathToFile}: {ex.Message}");
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,11 +354,8 @@ public class CfgManager{
|
|||
throw new FileNotFoundException($"The file at path {pathToFile} does not exist.");
|
||||
}
|
||||
|
||||
string key = Path.GetFullPath(pathToFile);
|
||||
object gate = _pathLocks.GetOrAdd(key, _ => new object());
|
||||
|
||||
lock (gate){
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
|
||||
lock (fileLock){
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Open, FileAccess.Read))
|
||||
using (var streamReader = new StreamReader(fileStream))
|
||||
using (var jsonReader = new JsonTextReader(streamReader)){
|
||||
var serializer = new JsonSerializer();
|
||||
|
|
@ -386,58 +369,12 @@ public class CfgManager{
|
|||
}
|
||||
|
||||
public static void DeleteFileIfExists(string pathToFile){
|
||||
string key = Path.GetFullPath(pathToFile);
|
||||
object gate = _pathLocks.GetOrAdd(key, _ => new object());
|
||||
|
||||
lock (gate){
|
||||
try{
|
||||
if (File.Exists(pathToFile)){
|
||||
DeleteFileWithRetry(pathToFile);
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred while deleting the file {pathToFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceFileWithRetry(string sourcePath, string destinationPath){
|
||||
const int maxAttempts = 5;
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++){
|
||||
try{
|
||||
if (File.Exists(destinationPath)){
|
||||
File.Replace(sourcePath, destinationPath, null, ignoreMetadataErrors: true);
|
||||
} else{
|
||||
File.Move(sourcePath, destinationPath, overwrite: true);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (IOException) when (attempt < maxAttempts){
|
||||
Thread.Sleep(100 * attempt);
|
||||
} catch (UnauthorizedAccessException) when (attempt < maxAttempts){
|
||||
Thread.Sleep(100 * attempt);
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(destinationPath)){
|
||||
File.Replace(sourcePath, destinationPath, null, ignoreMetadataErrors: true);
|
||||
} else{
|
||||
File.Move(sourcePath, destinationPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteFileWithRetry(string pathToFile){
|
||||
const int maxAttempts = 5;
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++){
|
||||
try{
|
||||
try{
|
||||
if (File.Exists(pathToFile)){
|
||||
File.Delete(pathToFile);
|
||||
return;
|
||||
} catch (IOException) when (attempt < maxAttempts){
|
||||
Thread.Sleep(100 * attempt);
|
||||
} catch (UnauthorizedAccessException) when (attempt < maxAttempts){
|
||||
Thread.Sleep(100 * attempt);
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred while deleting the file {pathToFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,11 +78,6 @@ public class HlsDownloader{
|
|||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
string fn = _data.OutputFile ?? string.Empty;
|
||||
|
||||
if (_data.M3U8Json != null && _newDownloadMethode){
|
||||
List<dynamic> segments = _data.M3U8Json.Segments;
|
||||
return await DownloadSegmentsBufferedResumeAsync(segments, fn);
|
||||
}
|
||||
|
||||
if (File.Exists(fn) && File.Exists($"{fn}.resume") && _data.Offset < 1){
|
||||
try{
|
||||
Console.WriteLine("Resume data found! Trying to resume...");
|
||||
|
|
@ -178,6 +173,11 @@ public class HlsDownloader{
|
|||
_data.Parts.Completed = _data.Offset;
|
||||
}
|
||||
|
||||
|
||||
if (_newDownloadMethode){
|
||||
return await DownloadSegmentsBufferedResumeAsync(segments, fn);
|
||||
}
|
||||
|
||||
for (int p = 0; p < Math.Ceiling((double)segments.Count / _data.Threads); p++){
|
||||
// Start time
|
||||
_data.DateStart = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
|
|
@ -314,7 +314,7 @@ public class HlsDownloader{
|
|||
return (Ok: false, _data.Parts);
|
||||
}
|
||||
|
||||
QueueManager.Instance.RefreshItem(_currentEpMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
await WaitWhilePausedAsync(_cancellationToken);
|
||||
if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){
|
||||
|
|
@ -413,7 +413,7 @@ public class HlsDownloader{
|
|||
return;
|
||||
}
|
||||
|
||||
QueueManager.Instance.RefreshItem(_currentEpMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
await WaitWhilePausedAsync(token);
|
||||
if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){
|
||||
|
|
@ -432,102 +432,25 @@ public class HlsDownloader{
|
|||
var totalSeg = _data.Parts.Total;
|
||||
string sessionId = Path.GetFileNameWithoutExtension(fn);
|
||||
string tempDir = Path.Combine(Path.GetDirectoryName(fn) ?? string.Empty, $"{sessionId}_temp");
|
||||
string resumeFile = $"{fn}.new.resume";
|
||||
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
string resumeFile = $"{fn}.new.resume";
|
||||
int downloadedParts = 0;
|
||||
int mergedParts = 0;
|
||||
bool hasBufferedResume = false;
|
||||
|
||||
if (File.Exists(resumeFile)){
|
||||
try{
|
||||
var resumeData = JsonConvert.DeserializeObject<BufferedResumeData>(File.ReadAllText(resumeFile));
|
||||
if (resumeData?.Total == totalSeg &&
|
||||
resumeData.DownloadedParts >= 0 &&
|
||||
resumeData.MergedParts >= 0 &&
|
||||
resumeData.DownloadedParts <= totalSeg &&
|
||||
resumeData.MergedParts <= resumeData.DownloadedParts){
|
||||
downloadedParts = resumeData.DownloadedParts;
|
||||
mergedParts = resumeData.MergedParts;
|
||||
hasBufferedResume = mergedParts == 0 || File.Exists(fn);
|
||||
} else{
|
||||
Console.WriteLine("Buffered resume data is wrong!");
|
||||
}
|
||||
var resumeData = JsonConvert.DeserializeObject<dynamic>(File.ReadAllText(resumeFile));
|
||||
downloadedParts = (int?)resumeData?.DownloadedParts ?? 0;
|
||||
mergedParts = (int?)resumeData?.MergedParts ?? 0;
|
||||
} catch{
|
||||
Console.WriteLine("Buffered resume data is wrong!");
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(fn) && !hasBufferedResume){
|
||||
string rwts = !string.IsNullOrEmpty(_data.Override) ? _data.Override : "Y";
|
||||
rwts = rwts.ToUpper();
|
||||
|
||||
if (rwts.StartsWith("Y")){
|
||||
Console.WriteLine($"Deleting «{fn}»...");
|
||||
File.Delete(fn);
|
||||
Helpers.DeleteFile($"{fn}.resume");
|
||||
CleanupNewDownloadMethod(tempDir, resumeFile);
|
||||
Directory.CreateDirectory(tempDir);
|
||||
downloadedParts = 0;
|
||||
mergedParts = 0;
|
||||
} else if (rwts.StartsWith("C")){
|
||||
return (Ok: true, _data.Parts);
|
||||
} else{
|
||||
return (Ok: false, _data.Parts);
|
||||
}
|
||||
} else if (File.Exists(resumeFile) && !hasBufferedResume){
|
||||
CleanupNewDownloadMethod(tempDir, resumeFile);
|
||||
Directory.CreateDirectory(tempDir);
|
||||
downloadedParts = 0;
|
||||
mergedParts = 0;
|
||||
hasBufferedResume = false;
|
||||
}
|
||||
|
||||
if (hasBufferedResume && File.Exists(fn) && mergedParts > 0){
|
||||
Console.WriteLine($"Adding content to «{fn}»...");
|
||||
|
||||
if (mergedParts == totalSeg){
|
||||
Console.WriteLine("Already finished");
|
||||
return (Ok: true, _data.Parts);
|
||||
}
|
||||
} else{
|
||||
if (hasBufferedResume){
|
||||
Console.WriteLine("Buffered resume data found! Trying to resume...");
|
||||
}
|
||||
|
||||
Console.WriteLine($"Saving stream to «{fn}»...");
|
||||
|
||||
if (!File.Exists(fn) && segments.Count > 0 && segments[0].map != null && !_data.SkipInit){
|
||||
Console.WriteLine("Download and save init part...");
|
||||
Segment initSeg = new Segment();
|
||||
initSeg.Uri = ObjectUtilities.GetMemberValue(segments[0].map, "uri");
|
||||
initSeg.Key = ObjectUtilities.GetMemberValue(segments[0].map, "key");
|
||||
initSeg.ByteRange = ObjectUtilities.GetMemberValue(segments[0].map, "byteRange");
|
||||
|
||||
if (ObjectUtilities.GetMemberValue(segments[0], "key") != null){
|
||||
initSeg.Key = segments[0].Key;
|
||||
}
|
||||
|
||||
try{
|
||||
var initDl = await DownloadPart(initSeg, 0, 0);
|
||||
await File.WriteAllBytesAsync(fn, initDl, _cancellationToken);
|
||||
await File.WriteAllTextAsync(resumeFile, JsonConvert.SerializeObject(new BufferedResumeData{
|
||||
DownloadedParts = 0,
|
||||
MergedParts = 0,
|
||||
Total = totalSeg
|
||||
}), _cancellationToken);
|
||||
Console.WriteLine("Init part downloaded.");
|
||||
} catch (Exception e){
|
||||
Console.Error.WriteLine($"Part init download error:\n\t{e.Message}");
|
||||
return (false, _data.Parts);
|
||||
}
|
||||
} else if (segments.Count > 0 && segments[0].map != null && _data.SkipInit){
|
||||
Console.WriteLine("Skipping init part can lead to broken video!");
|
||||
}
|
||||
}
|
||||
|
||||
_data.DateStart = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
if (downloadedParts > totalSeg) downloadedParts = totalSeg;
|
||||
if (mergedParts > downloadedParts) mergedParts = downloadedParts;
|
||||
|
||||
var semaphore = new SemaphoreSlim(_data.Threads);
|
||||
var downloadTasks = new List<Task>();
|
||||
|
|
@ -627,7 +550,7 @@ public class HlsDownloader{
|
|||
Doing = _isAudio ? "Merging Audio" : (_isVideo ? "Merging Video" : "")
|
||||
};
|
||||
|
||||
QueueManager.Instance.RefreshItem(_currentEpMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
|
||||
if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){
|
||||
CleanupBufferedArtifacts();
|
||||
|
|
@ -919,12 +842,6 @@ public class ResumeData{
|
|||
public int Completed{ get; set; }
|
||||
}
|
||||
|
||||
public class BufferedResumeData{
|
||||
public int Total{ get; set; }
|
||||
public int DownloadedParts{ get; set; }
|
||||
public int MergedParts{ get; set; }
|
||||
}
|
||||
|
||||
public class M3U8Json{
|
||||
public dynamic Segments{ get; set; } = new List<dynamic>();
|
||||
public int? MediaSequence{ get; set; }
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ public class HttpClientReq{
|
|||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.ProxyEnabled && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.ProxyHost)){
|
||||
handler = CreateHandler(true, CrunchyrollManager.Instance.CrunOptions.ProxySocks, CrunchyrollManager.Instance.CrunOptions.ProxyHost, CrunchyrollManager.Instance.CrunOptions.ProxyPort,
|
||||
CrunchyrollManager.Instance.CrunOptions.ProxyUsername, CrunchyrollManager.Instance.CrunOptions.ProxyPassword, CrunchyrollManager.Instance.CrunOptions.ProxyAllTraffic);
|
||||
CrunchyrollManager.Instance.CrunOptions.ProxyUsername, CrunchyrollManager.Instance.CrunOptions.ProxyPassword);
|
||||
string scheme = CrunchyrollManager.Instance.CrunOptions.ProxySocks ? "socks5" : "http";
|
||||
string proxyScope = CrunchyrollManager.Instance.CrunOptions.ProxyAllTraffic ? "all traffic" : "Crunchyroll traffic only";
|
||||
Console.Error.WriteLine($"Proxy is set for {proxyScope}: {scheme}://{CrunchyrollManager.Instance.CrunOptions.ProxyHost}:{CrunchyrollManager.Instance.CrunOptions.ProxyPort}");
|
||||
Console.Error.WriteLine($"Proxy is set: {scheme}://{CrunchyrollManager.Instance.CrunOptions.ProxyHost}:{CrunchyrollManager.Instance.CrunOptions.ProxyPort}");
|
||||
client = new HttpClient(handler);
|
||||
} else if (systemProxy != null){
|
||||
Uri testUri = new Uri("https://icanhazip.com");
|
||||
|
|
@ -94,7 +93,7 @@ public class HttpClientReq{
|
|||
};
|
||||
}
|
||||
|
||||
private HttpClientHandler CreateHandler(bool useProxy, bool useSocks = false, string? proxyHost = null, int proxyPort = 0, string? proxyUsername = "", string? proxyPassword = "", bool proxyAllTraffic = true){
|
||||
private HttpClientHandler CreateHandler(bool useProxy, bool useSocks = false, string? proxyHost = null, int proxyPort = 0, string? proxyUsername = "", string? proxyPassword = ""){
|
||||
var handler = new HttpClientHandler{
|
||||
CookieContainer = new CookieContainer(),
|
||||
UseCookies = true,
|
||||
|
|
@ -104,46 +103,15 @@ public class HttpClientReq{
|
|||
|
||||
if (useProxy && proxyHost != null){
|
||||
string scheme = useSocks ? "socks5" : "http";
|
||||
var proxy = new WebProxy($"{scheme}://{proxyHost}:{proxyPort}");
|
||||
proxy.Credentials = !string.IsNullOrEmpty(proxyUsername) && !string.IsNullOrEmpty(proxyPassword)
|
||||
? new NetworkCredential(proxyUsername, proxyPassword)
|
||||
: null;
|
||||
|
||||
handler.Proxy = proxyAllTraffic ? proxy : new CrunchyrollOnlyProxy(proxy);
|
||||
handler.Proxy = new WebProxy($"{scheme}://{proxyHost}:{proxyPort}");
|
||||
if (!string.IsNullOrEmpty(proxyUsername) && !string.IsNullOrEmpty(proxyPassword)){
|
||||
handler.Proxy.Credentials = new NetworkCredential(proxyUsername, proxyPassword);
|
||||
}
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private sealed class CrunchyrollOnlyProxy(WebProxy proxy): IWebProxy{
|
||||
public ICredentials? Credentials{
|
||||
get => proxy.Credentials;
|
||||
set => proxy.Credentials = value;
|
||||
}
|
||||
|
||||
public Uri GetProxy(Uri destination){
|
||||
return IsCrunchyrollProxyTarget(destination) ? proxy.GetProxy(destination) : destination;
|
||||
}
|
||||
|
||||
public bool IsBypassed(Uri host){
|
||||
return !IsCrunchyrollProxyTarget(host);
|
||||
}
|
||||
|
||||
private static bool IsCrunchyrollProxyTarget(Uri destination){
|
||||
if (!destination.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
|
||||
!destination.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)){
|
||||
return false;
|
||||
}
|
||||
|
||||
if (destination.ToString().Equals(ApiUrls.WidevineLicenceUrl, StringComparison.OrdinalIgnoreCase)){
|
||||
return false;
|
||||
}
|
||||
|
||||
return destination.Host.Equals("crunchyroll.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
destination.Host.EndsWith(".crunchyroll.com", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool IsOk, string ResponseContent, string error, Dictionary<string, string> Headers)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false, Dictionary<string, CookieCollection>? cookieStore = null,
|
||||
bool allowChallengeBypass = true){
|
||||
string content = string.Empty;
|
||||
|
|
@ -429,5 +397,5 @@ public static class ApiUrls{
|
|||
public static string authBasicMob = "Basic Ym1icmt4eXgzZDd1NmpzZnlsYTQ6QUlONEQ1VkVfY3Awd1Z6Zk5vUDBZcUhVcllGcDloU2c=";
|
||||
|
||||
public static readonly string MobileUserAgent = "Crunchyroll/3.81.6 Android/16";
|
||||
public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0";
|
||||
}
|
||||
public static readonly string FirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0";
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ public class FFmpegCommandBuilder : CommandBuilder{
|
|||
private readonly List<string> metaData = new();
|
||||
|
||||
private int index;
|
||||
private int videoIndex;
|
||||
private int audioIndex;
|
||||
private bool hasVideo;
|
||||
|
||||
|
|
@ -56,25 +55,14 @@ public class FFmpegCommandBuilder : CommandBuilder{
|
|||
Add($"-i \"{vid.Path}\"");
|
||||
|
||||
metaData.Add($"-map {index}:v");
|
||||
metaData.Add($"-metadata:s:v:{videoIndex} title=\"{vid.Language.Name}\"");
|
||||
AddVideoDisposition(vid);
|
||||
metaData.Add($"-metadata:s:v:{index} title=\"{vid.Language.Name}\"");
|
||||
|
||||
hasVideo = true;
|
||||
index++;
|
||||
videoIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVideoDisposition(MergerInput vid){
|
||||
if (Options.Defaults.Video?.Code == vid.Language.Code &&
|
||||
Options.Defaults.Video != Languages.DEFAULT_lang){
|
||||
metaData.Add($"-disposition:v:{videoIndex} default");
|
||||
} else{
|
||||
metaData.Add($"-disposition:v:{videoIndex} 0");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAudioInputs(){
|
||||
foreach (var aud in Options.OnlyAudio){
|
||||
if (aud.Delay is{ } delay && delay != 0){
|
||||
|
|
@ -261,4 +249,4 @@ public class FFmpegCommandBuilder : CommandBuilder{
|
|||
|
||||
File.WriteAllLines(chapterFilePath, ffmpegChapterLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CRD.Utils.Muxing.Structs;
|
||||
using CRD.Utils.Structs;
|
||||
|
|
@ -82,14 +81,7 @@ public class MkvMergeCommandBuilder(MergerOptions options) : CommandBuilder(opti
|
|||
}
|
||||
|
||||
private void AddVideoOnly(){
|
||||
var sorted = Options.OnlyVid
|
||||
.OrderBy(a => {
|
||||
var index = Options.DubLangList.IndexOf(a.Language.CrLocale);
|
||||
return index != -1 ? index : int.MaxValue;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var vid in sorted){
|
||||
foreach (var vid in Options.OnlyVid){
|
||||
if (!hasVideo || Options.KeepAllVideos){
|
||||
Add("--video-tracks 0");
|
||||
Add("--no-audio");
|
||||
|
|
@ -127,16 +119,6 @@ public class MkvMergeCommandBuilder(MergerOptions options) : CommandBuilder(opti
|
|||
private void AddTrackMetadata(string trackNum, LanguageItem lang){
|
||||
Add($"--track-name {trackNum}:\"{lang.Name}\"");
|
||||
Add($"--language {trackNum}:{lang.Code}");
|
||||
AddDefaultVideo(trackNum, lang);
|
||||
}
|
||||
|
||||
private void AddDefaultVideo(string trackNum, LanguageItem lang){
|
||||
if (Options.Defaults.Video?.Code == lang.Code &&
|
||||
Options.Defaults.Video != Languages.DEFAULT_lang){
|
||||
Add($"--default-track {trackNum}");
|
||||
} else{
|
||||
Add($"--default-track {trackNum}:0");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAudioMetadata(string trackNum, MergerInput track){
|
||||
|
|
@ -254,14 +236,9 @@ public class MkvMergeCommandBuilder(MergerOptions options) : CommandBuilder(opti
|
|||
var cover = Options.Cover.FirstOrDefault();
|
||||
|
||||
if (cover?.Path != null){
|
||||
if (!File.Exists(cover.Path)){
|
||||
Console.Error.WriteLine($"Cover file not found, skipping attachment: {cover.Path}");
|
||||
return;
|
||||
}
|
||||
|
||||
Add($"--attach-file \"{cover.Path}\"");
|
||||
Add("--attachment-mime-type image/png");
|
||||
Add("--attachment-name cover.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,11 @@ public class CrunchyMuxOptions{
|
|||
public bool MuxFonts{ get; set; }
|
||||
public bool MuxCover{ get; set; }
|
||||
public bool MuxDescription{ get; set; }
|
||||
public bool ReplaceExistingFiles{ get; set; }
|
||||
public string ForceMuxer{ get; set; }
|
||||
public bool NoCleanup{ get; set; }
|
||||
public string VideoTitle{ get; set; }
|
||||
public List<string> FfmpegOptions{ get; set; } = [];
|
||||
public List<string> MkvmergeOptions{ get; set; } = [];
|
||||
public LanguageItem? DefaultVideo{ get; set; }
|
||||
public LanguageItem? DefaultSub{ get; set; }
|
||||
public LanguageItem? DefaultAudio{ get; set; }
|
||||
public string CcTag{ get; set; }
|
||||
|
|
@ -33,4 +31,4 @@ public class CrunchyMuxOptions{
|
|||
public bool DefaultSubForcedDisplay{ get; set; }
|
||||
public bool CcSubsMuxingFlag{ get; set; }
|
||||
public bool SignsSubsAsForced{ get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ public class MergerOptions{
|
|||
}
|
||||
|
||||
public class Defaults{
|
||||
public LanguageItem? Video{ get; set; }
|
||||
public LanguageItem? Audio{ get; set; }
|
||||
public LanguageItem? Sub{ get; set; }
|
||||
}
|
||||
|
|
@ -37,4 +36,4 @@ public class Defaults{
|
|||
public class MuxOptions{
|
||||
public List<string>? Ffmpeg{ get; set; }
|
||||
public List<string>? Mkvmerge{ get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ public class SyncingHelper{
|
|||
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
|
||||
var ffmpegPath = CfgManager.PathFFMPEG;
|
||||
var arguments =
|
||||
$"{CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag}-ss {offset} -t {duration} -i \"{videoPath}\" -vf \"scale=256:144:force_original_aspect_ratio=decrease,pad=256:144:(ow-iw)/2:(oh-ih)/2,select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true \"{outputDir}\\frame%05d.jpg\"";
|
||||
$"{CrunchyrollManager.Instance.CrunOptions.FfmpegHwAccelFlag}-ss {offset} -t {duration} -i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true \"{outputDir}\\frame%05d.jpg\"";
|
||||
|
||||
var output = "";
|
||||
|
||||
|
|
|
|||
|
|
@ -31,22 +31,18 @@ public sealed class DownloadItemModelCollection{
|
|||
items.Clear();
|
||||
}
|
||||
|
||||
public void AddOrRefresh(CrunchyEpMeta queueItem){
|
||||
if (models.TryGetValue(queueItem, out var existingModel)){
|
||||
existingModel.Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
var newModel = new DownloadItemModel(queueItem);
|
||||
models.Add(queueItem, newModel);
|
||||
items.Add(newModel);
|
||||
|
||||
_ = newModel.LoadImage();
|
||||
}
|
||||
|
||||
public void SyncFromQueue(IEnumerable<CrunchyEpMeta> queueItems){
|
||||
foreach (var queueItem in queueItems){
|
||||
AddOrRefresh(queueItem);
|
||||
if (models.TryGetValue(queueItem, out var existingModel)){
|
||||
existingModel.Refresh();
|
||||
continue;
|
||||
}
|
||||
|
||||
var newModel = new DownloadItemModel(queueItem);
|
||||
models.Add(queueItem, newModel);
|
||||
items.Add(newModel);
|
||||
|
||||
_ = newModel.LoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ public sealed class QueuePersistenceManager : IDisposable{
|
|||
|
||||
public QueuePersistenceManager(QueueManager queueManager){
|
||||
this.queueManager = queueManager ?? throw new ArgumentNullException(nameof(queueManager));
|
||||
this.queueManager.QueuePersistenceRequested += OnQueuePersistenceRequested;
|
||||
this.queueManager.QueueStateChanged += OnQueueStateChanged;
|
||||
}
|
||||
|
||||
public void RestoreQueue(){
|
||||
|
|
@ -58,7 +58,7 @@ public sealed class QueuePersistenceManager : IDisposable{
|
|||
}
|
||||
}
|
||||
|
||||
private void OnQueuePersistenceRequested(object? sender, EventArgs e){
|
||||
private void OnQueueStateChanged(object? sender, EventArgs e){
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +119,6 @@ public sealed class QueuePersistenceManager : IDisposable{
|
|||
saveTimer = null;
|
||||
}
|
||||
|
||||
queueManager.QueuePersistenceRequested -= OnQueuePersistenceRequested;
|
||||
queueManager.QueueStateChanged -= OnQueueStateChanged;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils.Sonarr.Models;
|
||||
|
|
@ -22,11 +20,6 @@ public class SonarrClient{
|
|||
|
||||
private SonarrProperties properties;
|
||||
|
||||
private readonly SemaphoreSlim refreshLock = new(1, 1);
|
||||
private readonly TimeSpan sonarrSeriesCacheDuration = TimeSpan.FromMinutes(2);
|
||||
private DateTime lastSonarrSeriesRefreshUtc = DateTime.MinValue;
|
||||
private string? lastSonarrSeriesSettingsKey;
|
||||
|
||||
public List<SonarrSeries> SonarrSeries =[];
|
||||
|
||||
#region Singelton
|
||||
|
|
@ -55,64 +48,27 @@ public class SonarrClient{
|
|||
}
|
||||
|
||||
public async Task RefreshSonarr(){
|
||||
if (!await refreshLock.WaitAsync(0)){
|
||||
Console.WriteLine("[Sonarr Refresh] Skipped because another Sonarr refresh is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
await CheckSonarrSettings();
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
|
||||
SonarrSeries = await GetSeriesForRefresh();
|
||||
|
||||
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(true);
|
||||
|
||||
var matchedHistorySeries = CrunchyrollManager.Instance.HistoryList
|
||||
.Select(historySeries => new{
|
||||
HistorySeries = historySeries,
|
||||
IsValidSonarrSeriesId = int.TryParse(historySeries.SonarrSeriesId, out var sonarrSeriesId),
|
||||
SonarrSeriesId = sonarrSeriesId
|
||||
})
|
||||
.Where(item => item.IsValidSonarrSeriesId)
|
||||
.ToList();
|
||||
|
||||
using var throttler = new SemaphoreSlim(8);
|
||||
|
||||
var refreshTasks = matchedHistorySeries.Select(async item => {
|
||||
await throttler.WaitAsync();
|
||||
|
||||
try{
|
||||
var episodes = await GetEpisodes(item.SonarrSeriesId);
|
||||
item.HistorySeries.SonarrNextAirDate = CrunchyrollManager.Instance.History.GetNextAirDate(episodes);
|
||||
} finally{
|
||||
throttler.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(refreshTasks);
|
||||
await CheckSonarrSettings();
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
|
||||
SonarrSeries = await GetSeries();
|
||||
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(true);
|
||||
|
||||
foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){
|
||||
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
|
||||
List<SonarrEpisode>? episodes = await GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
|
||||
historySeries.SonarrNextAirDate = CrunchyrollManager.Instance.History.GetNextAirDate(episodes);
|
||||
}
|
||||
}
|
||||
} finally{
|
||||
refreshLock.Release();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshSonarrLite(){
|
||||
if (!await refreshLock.WaitAsync(0)){
|
||||
Console.WriteLine("[Sonarr Lite Refresh] Skipped because another Sonarr refresh is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
await CheckSonarrSettings();
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
|
||||
SonarrSeries = await GetSeriesForRefresh();
|
||||
|
||||
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(true);
|
||||
}
|
||||
} finally{
|
||||
refreshLock.Release();
|
||||
await CheckSonarrSettings();
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
|
||||
SonarrSeries = await GetSeries();
|
||||
CrunchyrollManager.Instance.History.MatchHistorySeriesWithSonarr(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,33 +79,6 @@ public class SonarrClient{
|
|||
apiUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}{(properties.UrlBase ?? "")}/api";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<SonarrSeries>> GetSeriesForRefresh(){
|
||||
var settingsKey = GetSonarrSettingsKey();
|
||||
|
||||
if (SonarrSeries.Count > 0 &&
|
||||
settingsKey == lastSonarrSeriesSettingsKey &&
|
||||
DateTime.UtcNow - lastSonarrSeriesRefreshUtc < sonarrSeriesCacheDuration){
|
||||
return SonarrSeries;
|
||||
}
|
||||
|
||||
var series = await GetSeries();
|
||||
lastSonarrSeriesRefreshUtc = DateTime.UtcNow;
|
||||
lastSonarrSeriesSettingsKey = settingsKey;
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
private string GetSonarrSettingsKey(){
|
||||
if (properties == null) return string.Empty;
|
||||
|
||||
return string.Join('|',
|
||||
properties.UseSsl,
|
||||
properties.Host ?? string.Empty,
|
||||
properties.Port,
|
||||
properties.UrlBase ?? string.Empty,
|
||||
properties.ApiKey ?? string.Empty);
|
||||
}
|
||||
|
||||
public async Task CheckSonarrSettings(){
|
||||
|
||||
|
|
@ -293,4 +222,4 @@ public class SonarrProperties(){
|
|||
|
||||
public bool UseSonarrNumbering{ get; set; }
|
||||
public bool SonarrEnabled{ get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.ComponentModel;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
|
|
@ -23,18 +22,7 @@ public class CalendarDay{
|
|||
public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
|
||||
}
|
||||
|
||||
public enum CalendarHistoryDownloadState{
|
||||
None,
|
||||
NotDownloaded,
|
||||
PartlyDownloaded,
|
||||
Downloaded
|
||||
}
|
||||
|
||||
public partial class CalendarEpisode : INotifyPropertyChanged{
|
||||
private bool _isInHistory;
|
||||
private bool _showHistoryMark = true;
|
||||
private CalendarHistoryDownloadState _historyDownloadState;
|
||||
|
||||
public DateTime DateTime{ get; set; }
|
||||
public bool? HasPassed{ get; set; }
|
||||
public string? EpisodeName{ get; set; }
|
||||
|
|
@ -52,79 +40,11 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
|||
|
||||
public string? CrSeriesID{ get; set; }
|
||||
|
||||
public string? CrSeasonID{ get; set; }
|
||||
|
||||
public string? CrEpisodeID{ get; set; }
|
||||
|
||||
public bool AnilistEpisode{ get; set; }
|
||||
|
||||
public bool FilteredOut{ get; set; }
|
||||
|
||||
public Locale? AudioLocale{ get; set; }
|
||||
|
||||
public List<CrBrowseEpisodeVersion>? Versions{ get; set; }
|
||||
|
||||
public string? OriginalEpisodeGuid{ get; set; }
|
||||
|
||||
public string? OriginalSeasonGuid{ get; set; }
|
||||
|
||||
public List<string> VersionGuids{ get; set; } =[];
|
||||
|
||||
public bool IsInHistory{
|
||||
get => _isInHistory;
|
||||
set{
|
||||
if (_isInHistory == value){
|
||||
return;
|
||||
}
|
||||
|
||||
_isInHistory = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsInHistory)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHistoryMarkVisible)));
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowHistoryMark{
|
||||
get => _showHistoryMark;
|
||||
set{
|
||||
if (_showHistoryMark == value){
|
||||
return;
|
||||
}
|
||||
|
||||
_showHistoryMark = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowHistoryMark)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHistoryMarkVisible)));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHistoryMarkVisible => IsInHistory && ShowHistoryMark && !AnilistEpisode;
|
||||
|
||||
public CalendarHistoryDownloadState HistoryDownloadState{
|
||||
get => _historyDownloadState;
|
||||
set{
|
||||
if (_historyDownloadState == value){
|
||||
return;
|
||||
}
|
||||
|
||||
_historyDownloadState = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HistoryDownloadState)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HistoryDownloadStatusBrush)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HistoryDownloadStatusTooltip)));
|
||||
}
|
||||
}
|
||||
|
||||
public IBrush HistoryDownloadStatusBrush => HistoryDownloadState switch{
|
||||
CalendarHistoryDownloadState.Downloaded => new SolidColorBrush(Color.Parse("#21a556")),
|
||||
CalendarHistoryDownloadState.PartlyDownloaded => new SolidColorBrush(Color.Parse("#f78c25")),
|
||||
CalendarHistoryDownloadState.NotDownloaded => Brushes.Gray,
|
||||
_ => Brushes.Transparent
|
||||
};
|
||||
|
||||
public string HistoryDownloadStatusTooltip => HistoryDownloadState switch{
|
||||
CalendarHistoryDownloadState.Downloaded => "Downloaded",
|
||||
CalendarHistoryDownloadState.PartlyDownloaded => "Partly downloaded",
|
||||
CalendarHistoryDownloadState.NotDownloaded => "Not downloaded",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
|
||||
|
||||
|
|
@ -160,4 +80,4 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
|||
Console.Error.WriteLine("Failed to load image: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -83,9 +83,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("download_only_with_all_selected_dubsub")]
|
||||
public bool DownloadOnlyWithAllSelectedDubSub{ get; set; }
|
||||
|
||||
[JsonProperty("replace_existing_files")]
|
||||
public bool ReplaceExistingFiles{ get; set; }
|
||||
|
||||
[JsonProperty("background_image_opacity")]
|
||||
public double BackgroundImageOpacity{ get; set; }
|
||||
|
||||
|
|
@ -107,9 +104,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("history_count_missing")]
|
||||
public bool HistoryCountMissing{ get; set; }
|
||||
|
||||
[JsonProperty("history_check_partial_downloads")]
|
||||
public bool HistoryCheckPartialDownloads{ get; set; }
|
||||
|
||||
[JsonProperty("history_include_cr_artists")]
|
||||
public bool HistoryIncludeCrArtists{ get; set; }
|
||||
|
||||
|
|
@ -171,9 +165,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("proxy_enabled")]
|
||||
public bool ProxyEnabled{ get; set; }
|
||||
|
||||
[JsonProperty("proxy_all_traffic")]
|
||||
public bool ProxyAllTraffic{ get; set; } = true;
|
||||
|
||||
[JsonProperty("proxy_socks")]
|
||||
public bool ProxySocks{ get; set; }
|
||||
|
||||
|
|
@ -256,11 +247,8 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("download_part_size")]
|
||||
public int Partsize{ get; set; }
|
||||
|
||||
[JsonProperty("download_delay_seconds")]
|
||||
public int DownloadDelaySeconds{ get; set; }
|
||||
|
||||
[JsonProperty("download_delay_use_dub_based")]
|
||||
public bool DownloadDelayUseDubBased{ get; set; }
|
||||
[JsonProperty("dub_download_delay_seconds")]
|
||||
public int DubDownloadDelaySeconds{ get; set; }
|
||||
|
||||
[JsonProperty("soft_subs")]
|
||||
public List<string> DlSubs{ get; set; } =[];
|
||||
|
|
@ -337,9 +325,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("mux_default_sub_forced_display")]
|
||||
public bool DefaultSubForcedDisplay{ get; set; }
|
||||
|
||||
[JsonProperty("mux_default_video")]
|
||||
public string DefaultVideo{ get; set; } = "";
|
||||
|
||||
[JsonProperty("mux_default_dub")]
|
||||
public string DefaultAudio{ get; set; } = "";
|
||||
|
||||
|
|
@ -394,9 +379,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("calendar_update_history")]
|
||||
public bool UpdateHistoryFromCalendar{ get; set; }
|
||||
|
||||
[JsonProperty("calendar_show_history_mark")]
|
||||
public bool CalendarShowHistoryMark{ get; set; } = true;
|
||||
|
||||
[JsonProperty("stream_endpoint_settings")]
|
||||
public CrAuthSettings? StreamEndpoint{ get; set; }
|
||||
|
||||
|
|
@ -406,15 +388,6 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("search_fetch_featured_music")]
|
||||
public bool SearchFetchFeaturedMusic{ get; set; }
|
||||
|
||||
[JsonProperty("add_download_search_add_to_history")]
|
||||
public bool AddDownloadSearchAddToHistory{ get; set; } = true;
|
||||
|
||||
[JsonProperty("add_download_single_episode_instant_add")]
|
||||
public bool AddDownloadSingleEpisodeInstantAdd{ get; set; } = true;
|
||||
|
||||
[JsonProperty("add_download_default_search_enabled")]
|
||||
public bool AddDownloadDefaultSearchEnabled{ get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public void NormalizeNotificationSettings(){
|
||||
|
|
|
|||
|
|
@ -246,16 +246,9 @@ public class CrunchyEpisode : IHistorySource{
|
|||
}
|
||||
|
||||
public bool IsSpecialSeason(){
|
||||
var seasonSpecificTitle = SeasonTitle;
|
||||
if (!string.IsNullOrWhiteSpace(SeriesTitle) &&
|
||||
seasonSpecificTitle.StartsWith(SeriesTitle, StringComparison.OrdinalIgnoreCase)){
|
||||
seasonSpecificTitle = seasonSpecificTitle[SeriesTitle.Length..];
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(
|
||||
seasonSpecificTitle,
|
||||
@"\b(OVA|Specials?|Extras?)\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)){
|
||||
if (SeasonTitle.Contains("OVA", StringComparison.Ordinal) ||
|
||||
SeasonTitle.Contains("Special", StringComparison.Ordinal) ||
|
||||
SeasonTitle.Contains("Extra", StringComparison.Ordinal)){
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -270,12 +263,7 @@ public class CrunchyEpisode : IHistorySource{
|
|||
}
|
||||
|
||||
public bool IsSpecialEpisode(){
|
||||
return Episode != null && !IsRegularEpisodeNumber(Episode);
|
||||
}
|
||||
|
||||
public static bool IsRegularEpisodeNumber(string? episode){
|
||||
return !string.IsNullOrWhiteSpace(episode) &&
|
||||
Regex.IsMatch(episode, @"^\d+(\.\d+)?(\s*-\s*\d+(\.\d+)?)?$");
|
||||
return !int.TryParse(Episode, out _);
|
||||
}
|
||||
|
||||
public List<string> GetAnimeIds(){
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ namespace CRD.Utils.Structs.Crunchyroll;
|
|||
public class StreamError{
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error{ get; set; }
|
||||
|
||||
[JsonPropertyName("reason")]
|
||||
public string? Reason{ get; set; }
|
||||
|
||||
[JsonPropertyName("activeStreams")]
|
||||
public List<ActiveStream> ActiveStreams{ get; set; } = new ();
|
||||
|
|
|
|||
|
|
@ -34,12 +34,6 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
[JsonProperty("episode_was_downloaded")]
|
||||
public bool WasDownloaded{ get; set; }
|
||||
|
||||
[JsonProperty("episode_downloaded_dub_lang")]
|
||||
public List<string> DownloadedDubLang{ get; set; } = [];
|
||||
|
||||
[JsonProperty("episode_downloaded_soft_subs")]
|
||||
public List<string> DownloadedSoftSubs{ get; set; } = [];
|
||||
|
||||
[JsonProperty("episode_tracked_series_release_notified")]
|
||||
public bool TrackedSeriesReleaseNotified{ get; set; }
|
||||
|
||||
|
|
@ -132,86 +126,14 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public bool IsPartiallyDownloaded(IEnumerable<string> requestedDubs, IEnumerable<string> requestedSoftSubs){
|
||||
return WasDownloaded &&
|
||||
(HasMissingAvailableItem(requestedDubs, DownloadedDubLang, HistoryEpisodeAvailableDubLang) ||
|
||||
HasMissingAvailableItem(requestedSoftSubs, DownloadedSoftSubs, HistoryEpisodeAvailableSoftSubs));
|
||||
}
|
||||
|
||||
public bool HasAvailableMissingDownloadedMedia(IEnumerable<string> requestedDubs, IEnumerable<string> requestedSoftSubs){
|
||||
return WasDownloaded &&
|
||||
(HasMissingAvailableItem(requestedDubs, DownloadedDubLang, HistoryEpisodeAvailableDubLang) ||
|
||||
HasMissingAvailableItem(requestedSoftSubs, DownloadedSoftSubs, HistoryEpisodeAvailableSoftSubs));
|
||||
}
|
||||
|
||||
private static bool HasMissingAvailableItem(IEnumerable<string> requested, List<string> downloaded, List<string> available){
|
||||
var requestedList = requested
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (requestedList.Count == 0 ||
|
||||
requestedList.Contains("none", StringComparer.OrdinalIgnoreCase) ||
|
||||
downloaded.Count == 0 ||
|
||||
available.Count == 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
var downloadedSet = downloaded.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var availableSet = available.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var requestedAvailableItems = requestedList.Contains("all", StringComparer.OrdinalIgnoreCase)
|
||||
? availableSet
|
||||
: requestedList.Where(availableSet.Contains);
|
||||
|
||||
return requestedAvailableItems.Any(item => !downloadedSet.Contains(item));
|
||||
}
|
||||
|
||||
private void NotifyDownloadStateChanged(){
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedDubLang)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedSoftSubs)));
|
||||
}
|
||||
|
||||
public void RefreshDownloadState(){
|
||||
NotifyDownloadStateChanged();
|
||||
}
|
||||
|
||||
private void NotifyAvailableMediaChanged(){
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HistoryEpisodeAvailableDubLang)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HistoryEpisodeAvailableSoftSubs)));
|
||||
}
|
||||
|
||||
public void SetDownloadedMedia(List<string> downloadedDubs, List<string> downloadedSoftSubs){
|
||||
WasDownloaded = true;
|
||||
DownloadedDubLang = MergeDownloadedLocales(DownloadedDubLang, downloadedDubs);
|
||||
DownloadedSoftSubs = MergeDownloadedLocales(DownloadedSoftSubs, downloadedSoftSubs);
|
||||
NotifyDownloadStateChanged();
|
||||
}
|
||||
|
||||
private static List<string> MergeDownloadedLocales(IEnumerable<string> existingLocales, IEnumerable<string> newLocales){
|
||||
return existingLocales
|
||||
.Concat(newLocales)
|
||||
.Where(locale => !string.IsNullOrWhiteSpace(locale))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void UpdateAvailableMedia(List<string> availableDubs, List<string> availableSoftSubs){
|
||||
HistoryEpisodeAvailableDubLang = availableDubs.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
HistoryEpisodeAvailableSoftSubs = availableSoftSubs.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
NotifyAvailableMediaChanged();
|
||||
}
|
||||
|
||||
public void ToggleWasDownloaded(){
|
||||
WasDownloaded = !WasDownloaded;
|
||||
ClearDownloadedMediaIfNotDownloaded();
|
||||
NotifyDownloadStateChanged();
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
|
||||
}
|
||||
|
||||
public void ToggleWasDownloadedSeries(HistorySeries? series){
|
||||
WasDownloaded = !WasDownloaded;
|
||||
ClearDownloadedMediaIfNotDownloaded();
|
||||
NotifyDownloadStateChanged();
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
|
||||
|
||||
if (series?.Seasons != null){
|
||||
foreach (var historySeason in series.Seasons){
|
||||
|
|
@ -224,14 +146,6 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
private void ClearDownloadedMediaIfNotDownloaded(){
|
||||
if (WasDownloaded)
|
||||
return;
|
||||
|
||||
DownloadedDubLang.Clear();
|
||||
DownloadedSoftSubs.Clear();
|
||||
}
|
||||
|
||||
public async Task DownloadEpisodeDefault(){
|
||||
await DownloadEpisode(EpisodeDownloadMode.Default,"",false);
|
||||
}
|
||||
|
|
@ -270,15 +184,4 @@ public class HistoryEpisode : INotifyPropertyChanged{
|
|||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText)));
|
||||
}
|
||||
|
||||
public void ClearSonarrEpisodeData(){
|
||||
SonarrEpisodeId = null;
|
||||
SonarrEpisodeNumber = null;
|
||||
SonarrHasFile = false;
|
||||
SonarrIsMonitored = false;
|
||||
SonarrAbsolutNumber = null;
|
||||
SonarrSeasonNumber = null;
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace CRD.Utils.Structs.History;
|
||||
|
||||
public static class HistoryOverrideOptions{
|
||||
public static ObservableCollection<string> GetVideoQualityList(StreamingService streamingService) =>
|
||||
streamingService switch{
|
||||
StreamingService.Crunchyroll => CrunchyrollVideoQualityList,
|
||||
_ => CrunchyrollVideoQualityList,
|
||||
};
|
||||
|
||||
public static ObservableCollection<string> GetDubLangList(StreamingService streamingService) =>
|
||||
streamingService switch{
|
||||
StreamingService.Crunchyroll => CrunchyrollDubLangList,
|
||||
_ => CrunchyrollDubLangList,
|
||||
};
|
||||
|
||||
public static ObservableCollection<string> GetSubLangList(StreamingService streamingService) =>
|
||||
streamingService switch{
|
||||
StreamingService.Crunchyroll => CrunchyrollSubLangList,
|
||||
_ => CrunchyrollSubLangList,
|
||||
};
|
||||
|
||||
private static ObservableCollection<string> CrunchyrollVideoQualityList{ get; } = new(){
|
||||
"best",
|
||||
"1080p",
|
||||
"720p",
|
||||
"480p",
|
||||
"360p",
|
||||
"240p",
|
||||
"worst",
|
||||
};
|
||||
|
||||
private static ObservableCollection<string> CrunchyrollDubLangList{ get; } = new(
|
||||
Languages.languages.Select(languageItem => languageItem.CrLocale)
|
||||
);
|
||||
|
||||
private static ObservableCollection<string> CrunchyrollSubLangList{ get; } = new(
|
||||
new[]{ "all", "none" }.Concat(Languages.languages.Select(languageItem => languageItem.CrLocale))
|
||||
);
|
||||
}
|
||||
|
|
@ -45,31 +45,24 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
[JsonIgnore]
|
||||
public bool IsExpanded{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public StreamingService StreamingService{ get; set; } = StreamingService.Unknown;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged(string propertyName){
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
#region Settings Override
|
||||
|
||||
[JsonIgnore]
|
||||
public string _selectedVideoQualityItem = "";
|
||||
public StringItem? _selectedVideoQualityItem;
|
||||
|
||||
[JsonIgnore]
|
||||
private bool Loading;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SelectedVideoQualityItem{
|
||||
public StringItem? SelectedVideoQualityItem{
|
||||
get => _selectedVideoQualityItem;
|
||||
set{
|
||||
_selectedVideoQualityItem = value ?? "";
|
||||
_selectedVideoQualityItem = value;
|
||||
|
||||
HistorySeasonVideoQualityOverride = _selectedVideoQualityItem;
|
||||
OnPropertyChanged(nameof(SelectedVideoQualityItem));
|
||||
HistorySeasonVideoQualityOverride = value?.stringValue ?? "";
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedVideoQualityItem)));
|
||||
if (!Loading){
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
|
@ -83,19 +76,31 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
public string SelectedDubs{ get; set; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> SelectedSubLang{ get; set; } = new();
|
||||
public ObservableCollection<StringItem> SelectedSubLang{ get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> SelectedDubLang{ get; set; } = new();
|
||||
public ObservableCollection<StringItem> SelectedDubLang{ get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> DubLangList => HistoryOverrideOptions.GetDubLangList(StreamingService);
|
||||
public ObservableCollection<StringItem> DubLangList{ get; } = new(){
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> SubLangList => HistoryOverrideOptions.GetSubLangList(StreamingService);
|
||||
public ObservableCollection<StringItem> SubLangList{ get; } = new(){
|
||||
new StringItem(){ stringValue = "all" },
|
||||
new StringItem(){ stringValue = "none" },
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> VideoQualityList => HistoryOverrideOptions.GetVideoQualityList(StreamingService);
|
||||
public ObservableCollection<StringItem> VideoQualityList{ get; } = new(){
|
||||
new StringItem(){ stringValue = "best" },
|
||||
new StringItem(){ stringValue = "1080p" },
|
||||
new StringItem(){ stringValue = "720p" },
|
||||
new StringItem(){ stringValue = "480p" },
|
||||
new StringItem(){ stringValue = "360p" },
|
||||
new StringItem(){ stringValue = "240p" },
|
||||
new StringItem(){ stringValue = "worst" },
|
||||
};
|
||||
|
||||
private void UpdateSubAndDubString(){
|
||||
HistorySeasonSoftSubsOverride.Clear();
|
||||
|
|
@ -103,13 +108,13 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
|
||||
if (SelectedSubLang.Count != 0){
|
||||
for (var i = 0; i < SelectedSubLang.Count; i++){
|
||||
HistorySeasonSoftSubsOverride.Add(SelectedSubLang[i]);
|
||||
HistorySeasonSoftSubsOverride.Add(SelectedSubLang[i].stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedDubLang.Count != 0){
|
||||
for (var i = 0; i < SelectedDubLang.Count; i++){
|
||||
HistorySeasonDubLangOverride.Add(SelectedDubLang[i]);
|
||||
HistorySeasonDubLangOverride.Add(SelectedDubLang[i].stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,8 +122,8 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
|
||||
|
||||
|
||||
OnPropertyChanged(nameof(SelectedSubs));
|
||||
OnPropertyChanged(nameof(SelectedDubs));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs)));
|
||||
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
|
@ -128,14 +133,18 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
}
|
||||
|
||||
public void Init(){
|
||||
SelectedSubLang.CollectionChanged -= Changes;
|
||||
SelectedDubLang.CollectionChanged -= Changes;
|
||||
|
||||
Loading = true;
|
||||
SelectedVideoQualityItem = VideoQualityList.FirstOrDefault(HistorySeasonVideoQualityOverride.Equals) ?? "";
|
||||
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
|
||||
foreach (var languageItem in Languages.languages){
|
||||
SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
|
||||
DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
|
||||
}
|
||||
}
|
||||
|
||||
var softSubLang = SubLangList.Where(HistorySeasonSoftSubsOverride.Contains).ToList();
|
||||
var dubLang = DubLangList.Where(HistorySeasonDubLangOverride.Contains).ToList();
|
||||
SelectedVideoQualityItem = VideoQualityList.FirstOrDefault(a => HistorySeasonVideoQualityOverride.Equals(a.stringValue)) ?? new StringItem(){ stringValue = "" };
|
||||
|
||||
var softSubLang = SubLangList.Where(a => HistorySeasonSoftSubsOverride.Contains(a.stringValue)).ToList();
|
||||
var dubLang = DubLangList.Where(a => HistorySeasonDubLangOverride.Contains(a.stringValue)).ToList();
|
||||
|
||||
SelectedSubLang.Clear();
|
||||
foreach (var listBoxItem in softSubLang){
|
||||
|
|
@ -164,18 +173,18 @@ public class HistorySeason : INotifyPropertyChanged{
|
|||
}
|
||||
|
||||
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
|
||||
OnPropertyChanged(nameof(DownloadedEpisodes));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
public void UpdateDownloaded(){
|
||||
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
|
||||
OnPropertyChanged(nameof(DownloadedEpisodes));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
public void UpdateDownloadedSilent(){
|
||||
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
|
||||
OnPropertyChanged(nameof(DownloadedEpisodes));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,10 +77,6 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged(string propertyName){
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Bitmap? ThumbnailImage{ get; set; }
|
||||
|
||||
|
|
@ -99,7 +95,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
set{
|
||||
if (_editModeEnabled != value){
|
||||
_editModeEnabled = value;
|
||||
OnPropertyChanged(nameof(EditModeEnabled));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditModeEnabled)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -119,16 +115,16 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
private bool Loading;
|
||||
|
||||
[JsonIgnore]
|
||||
public string _selectedVideoQualityItem = "";
|
||||
public StringItem? _selectedVideoQualityItem;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SelectedVideoQualityItem{
|
||||
public StringItem? SelectedVideoQualityItem{
|
||||
get => _selectedVideoQualityItem;
|
||||
set{
|
||||
_selectedVideoQualityItem = value ?? "";
|
||||
_selectedVideoQualityItem = value;
|
||||
|
||||
HistorySeriesVideoQualityOverride = _selectedVideoQualityItem;
|
||||
OnPropertyChanged(nameof(SelectedVideoQualityItem));
|
||||
HistorySeriesVideoQualityOverride = value?.stringValue ?? "";
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedVideoQualityItem)));
|
||||
if (!Loading){
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
|
@ -142,20 +138,32 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
public string SelectedDubs{ get; set; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> SelectedSubLang{ get; set; } = new();
|
||||
public ObservableCollection<StringItem> SelectedSubLang{ get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> SelectedDubLang{ get; set; } = new();
|
||||
public ObservableCollection<StringItem> SelectedDubLang{ get; set; } = new();
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> DubLangList => HistoryOverrideOptions.GetDubLangList(SeriesStreamingService);
|
||||
public ObservableCollection<StringItem> DubLangList{ get; } = new(){
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> SubLangList => HistoryOverrideOptions.GetSubLangList(SeriesStreamingService);
|
||||
public ObservableCollection<StringItem> SubLangList{ get; } = new(){
|
||||
new StringItem(){ stringValue = "all" },
|
||||
new StringItem(){ stringValue = "none" },
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<string> VideoQualityList => HistoryOverrideOptions.GetVideoQualityList(SeriesStreamingService);
|
||||
public ObservableCollection<StringItem> VideoQualityList{ get; } = new(){
|
||||
new StringItem(){ stringValue = "best" },
|
||||
new StringItem(){ stringValue = "1080p" },
|
||||
new StringItem(){ stringValue = "720p" },
|
||||
new StringItem(){ stringValue = "480p" },
|
||||
new StringItem(){ stringValue = "360p" },
|
||||
new StringItem(){ stringValue = "240p" },
|
||||
new StringItem(){ stringValue = "worst" },
|
||||
};
|
||||
|
||||
private void UpdateSubAndDubString(){
|
||||
HistorySeriesSoftSubsOverride.Clear();
|
||||
|
|
@ -163,21 +171,21 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
if (SelectedSubLang.Count != 0){
|
||||
for (var i = 0; i < SelectedSubLang.Count; i++){
|
||||
HistorySeriesSoftSubsOverride.Add(SelectedSubLang[i]);
|
||||
HistorySeriesSoftSubsOverride.Add(SelectedSubLang[i].stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedDubLang.Count != 0){
|
||||
for (var i = 0; i < SelectedDubLang.Count; i++){
|
||||
HistorySeriesDubLangOverride.Add(SelectedDubLang[i]);
|
||||
HistorySeriesDubLangOverride.Add(SelectedDubLang[i].stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
SelectedDubs = string.Join(", ", HistorySeriesDubLangOverride) ?? "";
|
||||
SelectedSubs = string.Join(", ", HistorySeriesSoftSubsOverride) ?? "";
|
||||
|
||||
OnPropertyChanged(nameof(SelectedSubs));
|
||||
OnPropertyChanged(nameof(SelectedDubs));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs)));
|
||||
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
|
@ -187,14 +195,18 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
}
|
||||
|
||||
public void Init(){
|
||||
SelectedSubLang.CollectionChanged -= Changes;
|
||||
SelectedDubLang.CollectionChanged -= Changes;
|
||||
|
||||
Loading = true;
|
||||
SelectedVideoQualityItem = VideoQualityList.FirstOrDefault(HistorySeriesVideoQualityOverride.Equals) ?? "";
|
||||
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
|
||||
foreach (var languageItem in Languages.languages){
|
||||
SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
|
||||
DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
|
||||
}
|
||||
}
|
||||
|
||||
var softSubLang = SubLangList.Where(HistorySeriesSoftSubsOverride.Contains).ToList();
|
||||
var dubLang = DubLangList.Where(HistorySeriesDubLangOverride.Contains).ToList();
|
||||
SelectedVideoQualityItem = VideoQualityList.FirstOrDefault(a => HistorySeriesVideoQualityOverride.Equals(a.stringValue)) ?? new StringItem(){ stringValue = "" };
|
||||
|
||||
var softSubLang = SubLangList.Where(a => HistorySeriesSoftSubsOverride.Contains(a.stringValue)).ToList();
|
||||
var dubLang = DubLangList.Where(a => HistorySeriesDubLangOverride.Contains(a.stringValue)).ToList();
|
||||
|
||||
SelectedSubLang.Clear();
|
||||
foreach (var listBoxItem in softSubLang){
|
||||
|
|
@ -213,18 +225,10 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
SelectedDubLang.CollectionChanged += Changes;
|
||||
|
||||
UpdateSeriesFolderPath();
|
||||
InitSeasons();
|
||||
|
||||
Loading = false;
|
||||
}
|
||||
|
||||
private void InitSeasons(){
|
||||
foreach (var season in Seasons){
|
||||
season.StreamingService = SeriesStreamingService;
|
||||
season.Init();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task LoadImage(){
|
||||
|
|
@ -235,7 +239,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
ThumbnailImage = await Helpers.LoadImage(ThumbnailImageUrl);
|
||||
IsImageLoaded = true;
|
||||
|
||||
OnPropertyChanged(nameof(ThumbnailImage));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine("Failed to load image: " + ex.Message);
|
||||
}
|
||||
|
|
@ -243,7 +247,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
public void UpdateNewEpisodes(){
|
||||
NewEpisodes = EnumerateEpisodes().Count();
|
||||
OnPropertyChanged(nameof(NewEpisodes));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
|
||||
}
|
||||
|
||||
public async Task AddNewMissingToDownloads(bool checkQueueForId = false){
|
||||
|
|
@ -278,7 +282,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
if (skipUnmonitored && sonarrEnabled && !ep.SonarrIsMonitored)
|
||||
continue;
|
||||
|
||||
if (ShouldCountEpisode(season, ep, useSonarr, countMissing, false))
|
||||
if (ShouldCountEpisode(ep, useSonarr, countMissing, false))
|
||||
yield return ep;
|
||||
}
|
||||
}
|
||||
|
|
@ -294,90 +298,44 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
if (ep.SpecialEpisode){
|
||||
if (historyAddSpecials &&
|
||||
ShouldCountEpisode(season, ep, useSonarr, countMissing, false)){
|
||||
ShouldCountEpisode(ep, useSonarr, countMissing, false)){
|
||||
yield return ep;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ShouldCountEpisode(season, ep, useSonarr, countMissing, foundWatched)){
|
||||
if (ShouldCountEpisode(ep, useSonarr, countMissing, foundWatched)){
|
||||
yield return ep;
|
||||
} else{
|
||||
if (ep.WasDownloaded && !IsPartialDownloadActionable(season, ep)){
|
||||
foundWatched = true;
|
||||
}
|
||||
foundWatched = true;
|
||||
|
||||
if (!historyAddSpecials && !countMissing)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWatched && !historyAddSpecials && !countMissing)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldCountEpisode(HistorySeason season, HistoryEpisode episode, bool useSonarr, bool countMissing, bool foundWatched){
|
||||
private bool ShouldCountEpisode(HistoryEpisode episode, bool useSonarr, bool countMissing, bool foundWatched){
|
||||
if (useSonarr)
|
||||
return !string.IsNullOrEmpty(episode.SonarrEpisodeId) && !episode.SonarrHasFile;
|
||||
|
||||
return IsPartialDownloadActionable(season, episode) ||
|
||||
!episode.WasDownloaded && (!foundWatched || countMissing);
|
||||
return !episode.WasDownloaded && (!foundWatched || countMissing);
|
||||
}
|
||||
|
||||
private bool IsPartialDownloadActionable(HistorySeason season, HistoryEpisode episode){
|
||||
if (!CrunchyrollManager.Instance.CrunOptions.HistoryCheckPartialDownloads){
|
||||
return false;
|
||||
}
|
||||
|
||||
var requestedDubs = GetEffectiveDubLang(this, season);
|
||||
var requestedSoftSubs = GetEffectiveSoftSubs(this, season, episode);
|
||||
|
||||
return episode.HasAvailableMissingDownloadedMedia(requestedDubs, requestedSoftSubs);
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetEffectiveDubLang(HistorySeries? series, HistorySeason? season){
|
||||
if (season?.HistorySeasonDubLangOverride.Count > 0){
|
||||
return season.HistorySeasonDubLangOverride;
|
||||
}
|
||||
|
||||
if (series?.HistorySeriesDubLangOverride.Count > 0){
|
||||
return series.HistorySeriesDubLangOverride;
|
||||
}
|
||||
|
||||
return series?.SeriesStreamingService == StreamingService.Crunchyroll
|
||||
? CrunchyrollManager.Instance.CrunOptions.DubLang
|
||||
: [];
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetEffectiveSoftSubs(HistorySeries? series, HistorySeason? season, HistoryEpisode episode){
|
||||
IEnumerable<string> requested;
|
||||
|
||||
if (season?.HistorySeasonSoftSubsOverride.Count > 0){
|
||||
requested = season.HistorySeasonSoftSubsOverride;
|
||||
} else if (series?.HistorySeriesSoftSubsOverride.Count > 0){
|
||||
requested = series.HistorySeriesSoftSubsOverride;
|
||||
} else{
|
||||
requested = series?.SeriesStreamingService == StreamingService.Crunchyroll
|
||||
? CrunchyrollManager.Instance.CrunOptions.DlSubs
|
||||
: [];
|
||||
}
|
||||
|
||||
var requestedList = requested.ToList();
|
||||
|
||||
if (requestedList.Contains("none", StringComparer.OrdinalIgnoreCase)){
|
||||
return [];
|
||||
}
|
||||
|
||||
return requestedList.Contains("all", StringComparer.OrdinalIgnoreCase)
|
||||
? episode.HistoryEpisodeAvailableSoftSubs
|
||||
: requestedList;
|
||||
}
|
||||
|
||||
public void SetFetchingData(bool fetchingData = true){
|
||||
FetchingData = fetchingData;
|
||||
OnPropertyChanged(nameof(FetchingData));
|
||||
public void SetFetchingData(){
|
||||
FetchingData = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
||||
}
|
||||
|
||||
public async Task<bool> FetchData(string? seasonId){
|
||||
Console.WriteLine($"Fetching Data for: {SeriesTitle}");
|
||||
FetchingData = true;
|
||||
OnPropertyChanged(nameof(FetchingData));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
||||
var isOk = true;
|
||||
|
||||
switch (SeriesType){
|
||||
|
|
@ -408,11 +366,11 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
}
|
||||
|
||||
|
||||
OnPropertyChanged(nameof(SeriesTitle));
|
||||
OnPropertyChanged(nameof(SeriesDescription));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesTitle)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription)));
|
||||
UpdateNewEpisodes();
|
||||
FetchingData = false;
|
||||
OnPropertyChanged(nameof(FetchingData));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
|
||||
|
||||
return isOk;
|
||||
}
|
||||
|
|
@ -421,7 +379,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
HistorySeason? objectToRemove = Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null;
|
||||
if (objectToRemove != null){
|
||||
Seasons.Remove(objectToRemove);
|
||||
OnPropertyChanged(nameof(Seasons));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Seasons)));
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
}
|
||||
|
|
@ -459,7 +417,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
if (!string.IsNullOrEmpty(SeriesDownloadPath) && Directory.Exists(SeriesDownloadPath)){
|
||||
SeriesFolderPath = SeriesDownloadPath;
|
||||
SeriesFolderPathExists = true;
|
||||
OnPropertyChanged(nameof(SeriesFolderPathExists));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -473,7 +431,7 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
if (!string.IsNullOrEmpty(parentFolder) && Directory.Exists(parentFolder)){
|
||||
SeriesFolderPath = parentFolder;
|
||||
SeriesFolderPathExists = true;
|
||||
OnPropertyChanged(nameof(SeriesFolderPathExists));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||
return;
|
||||
}
|
||||
} catch (Exception e){
|
||||
|
|
@ -483,14 +441,14 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
// Auto generated path
|
||||
if (string.IsNullOrEmpty(SeriesTitle)){
|
||||
OnPropertyChanged(nameof(SeriesFolderPathExists));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||
return;
|
||||
}
|
||||
|
||||
var seriesTitle = FileNameManager.CleanupFilename(SeriesTitle);
|
||||
|
||||
if (string.IsNullOrEmpty(seriesTitle)){
|
||||
OnPropertyChanged(nameof(SeriesFolderPathExists));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -506,6 +464,6 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
SeriesFolderPathExists = true;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(SeriesFolderPathExists));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
||||
namespace CRD.Utils.UI;
|
||||
|
||||
public class UiHistoryDownloadStatusConverter : IMultiValueConverter{
|
||||
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture){
|
||||
var episode = values.Count > 0 && values[0] is HistoryEpisode ep ? ep : null;
|
||||
var series = values.Count > 1 && values[1] is HistorySeries hs ? hs : null;
|
||||
var season = values.Count > 2 && values[2] is HistorySeason hsn ? hsn : null;
|
||||
|
||||
var isTooltip = string.Equals(parameter?.ToString(), "Tooltip", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (episode == null || !episode.WasDownloaded){
|
||||
return isTooltip ? "Mark as downloaded" : Brushes.Gray;
|
||||
}
|
||||
|
||||
var requestedDubs = Enumerable.Empty<string>();
|
||||
var requestedSoftSubs = Enumerable.Empty<string>();
|
||||
var isPartial = false;
|
||||
if (CrunchyrollManager.Instance.CrunOptions.HistoryCheckPartialDownloads){
|
||||
requestedDubs = HistorySeries.GetEffectiveDubLang(series, season);
|
||||
requestedSoftSubs = HistorySeries.GetEffectiveSoftSubs(series, season, episode);
|
||||
isPartial = episode.IsPartiallyDownloaded(requestedDubs, requestedSoftSubs);
|
||||
}
|
||||
|
||||
if (isTooltip){
|
||||
return BuildTooltip(episode, requestedDubs, requestedSoftSubs, isPartial);
|
||||
}
|
||||
|
||||
return isPartial ? new SolidColorBrush(Color.Parse("#f78c25")) : new SolidColorBrush(Color.Parse("#21a556"));
|
||||
}
|
||||
|
||||
public IList<object> ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
private static string BuildTooltip(
|
||||
HistoryEpisode episode,
|
||||
IEnumerable<string> requestedDubs,
|
||||
IEnumerable<string> requestedSoftSubs,
|
||||
bool isPartial){
|
||||
if (!isPartial){
|
||||
return "Downloaded";
|
||||
}
|
||||
|
||||
var missingDubs = GetMissingLocales(
|
||||
requestedDubs,
|
||||
episode.DownloadedDubLang,
|
||||
episode.HistoryEpisodeAvailableDubLang);
|
||||
var missingSubs = GetMissingLocales(
|
||||
requestedSoftSubs,
|
||||
episode.DownloadedSoftSubs,
|
||||
episode.HistoryEpisodeAvailableSoftSubs);
|
||||
var lines = new List<string>{ "Downloaded, but missing:" };
|
||||
|
||||
if (missingDubs.Count > 0){
|
||||
lines.Add($"Dubs: {string.Join(", ", missingDubs)}");
|
||||
}
|
||||
|
||||
if (missingSubs.Count > 0){
|
||||
lines.Add($"Subs: {string.Join(", ", missingSubs)}");
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
private static List<string> GetMissingLocales(
|
||||
IEnumerable<string> requested,
|
||||
IEnumerable<string> downloaded,
|
||||
IEnumerable<string> available){
|
||||
var requestedList = requested
|
||||
.Where(locale => !string.IsNullOrWhiteSpace(locale))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (requestedList.Count == 0 ||
|
||||
requestedList.Contains("none", StringComparer.OrdinalIgnoreCase)){
|
||||
return [];
|
||||
}
|
||||
|
||||
var downloadedSet = downloaded.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var availableList = available
|
||||
.Where(locale => !string.IsNullOrWhiteSpace(locale))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var requestedAvailableLocales = requestedList.Contains("all", StringComparer.OrdinalIgnoreCase)
|
||||
? availableList
|
||||
: requestedList.Where(locale => availableList.Contains(locale, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
return requestedAvailableLocales
|
||||
.Where(locale => !downloadedSet.Contains(locale))
|
||||
.OrderBy(locale => locale, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,73 +5,67 @@ using System.Collections.Specialized;
|
|||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll.Music;
|
||||
using CRD.Views;
|
||||
using ReactiveUI;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace CRD.ViewModels;
|
||||
|
||||
public partial class AddDownloadPageViewModel : ViewModelBase{
|
||||
[ObservableProperty]
|
||||
private string urlInput = "";
|
||||
private string _urlInput = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string buttonText = "Enter Url";
|
||||
private string _buttonText = "Enter Url";
|
||||
|
||||
[ObservableProperty]
|
||||
private string buttonTextSelectSeason = "Select Season";
|
||||
private string _buttonTextSelectSeason = "Select Season";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool addAllEpisodes;
|
||||
private bool _addAllEpisodes;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool buttonEnabled;
|
||||
private bool _buttonEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool allButtonEnabled;
|
||||
private bool _allButtonEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool showLoading;
|
||||
private bool _showLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool searchEnabled;
|
||||
private bool _searchEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool defaultSearchEnabled;
|
||||
private bool _searchVisible = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool addSearchResultsToHistory = true;
|
||||
private bool _slectSeasonVisible;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool singleEpisodeInstantAdd = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool searchVisible = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool slectSeasonVisible;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool searchPopupVisible;
|
||||
private bool _searchPopupVisible;
|
||||
|
||||
public ObservableCollection<ItemModel> Items{ get; set; } = new();
|
||||
public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new();
|
||||
public ObservableCollection<ItemModel> SelectedItems{ get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private CrBrowseSeries selectedSearchItem;
|
||||
public CrBrowseSeries _selectedSearchItem;
|
||||
|
||||
[ObservableProperty]
|
||||
private ComboBoxItem currentSelectedSeason;
|
||||
public ComboBoxItem _currentSelectedSeason;
|
||||
|
||||
public ObservableCollection<ComboBoxItem> SeasonList{ get; set; } = new();
|
||||
|
||||
|
|
@ -85,22 +79,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
private bool CurrentSeasonFullySelected;
|
||||
|
||||
private string currentSingleEpisodeId = "";
|
||||
private string currentSingleEpisodeLocale = "";
|
||||
private bool settingsLoaded;
|
||||
|
||||
public string SearchWatermark => SearchEnabled
|
||||
? "Search for a series title"
|
||||
: "Enter series or episode URL";
|
||||
|
||||
public AddDownloadPageViewModel(){
|
||||
var options = CrunchyrollManager.Instance.CrunOptions;
|
||||
AddSearchResultsToHistory = options.AddDownloadSearchAddToHistory;
|
||||
SingleEpisodeInstantAdd = options.AddDownloadSingleEpisodeInstantAdd;
|
||||
DefaultSearchEnabled = options.AddDownloadDefaultSearchEnabled;
|
||||
SearchEnabled = DefaultSearchEnabled;
|
||||
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
|
||||
settingsLoaded = true;
|
||||
}
|
||||
|
||||
private async Task UpdateSearch(string value){
|
||||
|
|
@ -188,31 +168,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
partial void OnSearchEnabledChanged(bool value){
|
||||
ButtonText = SearchEnabled ? "Select Searched Series" : "Enter Url";
|
||||
ButtonEnabled = false;
|
||||
RaisePropertyChanged(nameof(SearchWatermark));
|
||||
}
|
||||
|
||||
partial void OnDefaultSearchEnabledChanged(bool value){
|
||||
UpdateAddDownloadSettings();
|
||||
}
|
||||
|
||||
partial void OnAddSearchResultsToHistoryChanged(bool value){
|
||||
UpdateAddDownloadSettings();
|
||||
}
|
||||
|
||||
partial void OnSingleEpisodeInstantAddChanged(bool value){
|
||||
UpdateAddDownloadSettings();
|
||||
}
|
||||
|
||||
private void UpdateAddDownloadSettings(){
|
||||
if (!settingsLoaded){
|
||||
return;
|
||||
}
|
||||
|
||||
var options = CrunchyrollManager.Instance.CrunOptions;
|
||||
options.AddDownloadSearchAddToHistory = AddSearchResultsToHistory;
|
||||
options.AddDownloadSingleEpisodeInstantAdd = SingleEpisodeInstantAdd;
|
||||
options.AddDownloadDefaultSearchEnabled = DefaultSearchEnabled;
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
||||
#region OnButtonPress
|
||||
|
|
@ -226,10 +181,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
AddSelectedMusicVideosToQueue();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(currentSingleEpisodeId)){
|
||||
await AddSelectedSingleEpisodeToQueue();
|
||||
}
|
||||
|
||||
if (currentSeriesList != null){
|
||||
await AddSelectedEpisodesToQueue();
|
||||
}
|
||||
|
|
@ -292,8 +243,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
private void ResetState(){
|
||||
currentMusicVideoList = null;
|
||||
currentSingleEpisodeId = "";
|
||||
currentSingleEpisodeLocale = "";
|
||||
UrlInput = "";
|
||||
selectedEpisodes.Clear();
|
||||
SelectedItems.Clear();
|
||||
|
|
@ -303,8 +252,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
episodesBySeason.Clear();
|
||||
AllButtonEnabled = false;
|
||||
AddAllEpisodes = false;
|
||||
SearchEnabled = DefaultSearchEnabled;
|
||||
ButtonText = SearchEnabled ? "Select Searched Series" : "Enter Url";
|
||||
ButtonText = "Enter Url";
|
||||
ButtonEnabled = false;
|
||||
SearchVisible = true;
|
||||
SlectSeasonVisible = false;
|
||||
|
|
@ -320,8 +268,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
var matchResult = ExtractLocaleAndIdFromUrl();
|
||||
|
||||
if (matchResult is ({ } locale, { } id, { } urlType)){
|
||||
switch (urlType){
|
||||
if (matchResult is ({ } locale, { } id)){
|
||||
switch (GetUrlType()){
|
||||
case CrunchyUrlType.Artist:
|
||||
await HandleArtistUrlAsync(locale, id);
|
||||
break;
|
||||
|
|
@ -332,7 +280,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
HandleConcertUrl(id);
|
||||
break;
|
||||
case CrunchyUrlType.Episode:
|
||||
await HandleEpisodeUrlAsync(locale, id);
|
||||
HandleEpisodeUrl(locale, id);
|
||||
break;
|
||||
case CrunchyUrlType.Series:
|
||||
await HandleSeriesUrlAsync(locale, id);
|
||||
|
|
@ -344,69 +292,23 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
private (string locale, string id, CrunchyUrlType urlType)? ExtractLocaleAndIdFromUrl(){
|
||||
return TryExtractCrunchyUrlParts(UrlInput);
|
||||
}
|
||||
private (string locale, string id)? ExtractLocaleAndIdFromUrl(){
|
||||
var match = Regex.Match(UrlInput, @"^(?:https?:\/\/[^/]+)?(?:\/([a-z]{2}))?\/(?:[^/]+\/)?(artist|watch|series)(?:\/(musicvideo|concert))?\/([^/]+)(?:\/[^/]*)?$");
|
||||
|
||||
internal static (string locale, string id, CrunchyUrlType urlType)? TryExtractCrunchyUrlParts(string? urlInput){
|
||||
if (string.IsNullOrWhiteSpace(urlInput)){
|
||||
return null;
|
||||
}
|
||||
|
||||
var input = urlInput.Trim();
|
||||
var path = Uri.TryCreate(input, UriKind.Absolute, out var uri)
|
||||
? uri.AbsolutePath
|
||||
: input.Split(['?', '#'], 2)[0];
|
||||
|
||||
var segments = path
|
||||
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList();
|
||||
|
||||
var typeIndex = segments.FindIndex(IsCrunchyUrlTypeSegment);
|
||||
if (typeIndex < 0){
|
||||
return null;
|
||||
}
|
||||
|
||||
var typeSegment = segments[typeIndex];
|
||||
var idIndex = typeIndex + 1;
|
||||
var urlType = typeSegment switch{
|
||||
"artist" => CrunchyUrlType.Artist,
|
||||
"series" => CrunchyUrlType.Series,
|
||||
"watch" when idIndex < segments.Count && segments[idIndex] == "musicvideo" => CrunchyUrlType.MusicVideo,
|
||||
"watch" when idIndex < segments.Count && segments[idIndex] == "concert" => CrunchyUrlType.Concert,
|
||||
"watch" => CrunchyUrlType.Episode,
|
||||
_ => CrunchyUrlType.Unknown,
|
||||
};
|
||||
|
||||
if (urlType is CrunchyUrlType.MusicVideo or CrunchyUrlType.Concert){
|
||||
idIndex++;
|
||||
}
|
||||
|
||||
if (idIndex >= segments.Count || string.IsNullOrWhiteSpace(segments[idIndex])){
|
||||
return null;
|
||||
}
|
||||
|
||||
var locale = typeIndex > 0 && IsLocaleSegment(segments[0])
|
||||
? segments[0]
|
||||
: "";
|
||||
|
||||
return (locale, segments[idIndex], urlType);
|
||||
return match.Success
|
||||
? (match.Groups[1].Value ?? "", match.Groups[4].Value)
|
||||
: null;
|
||||
}
|
||||
|
||||
private CrunchyUrlType GetUrlType(){
|
||||
return TryExtractCrunchyUrlParts(UrlInput)?.urlType ?? CrunchyUrlType.Unknown;
|
||||
}
|
||||
|
||||
private static bool IsCrunchyUrlTypeSegment(string segment){
|
||||
return segment is "artist" or "watch" or "series";
|
||||
}
|
||||
|
||||
private static bool IsLocaleSegment(string segment){
|
||||
return segment.Length == 2 && segment.All(char.IsAsciiLetterLower)
|
||||
|| segment.Length == 5
|
||||
&& segment[..2].All(char.IsAsciiLetterLower)
|
||||
&& segment[2] == '-'
|
||||
&& segment[3..].All(char.IsAsciiLetterUpper);
|
||||
return UrlInput switch{
|
||||
_ when UrlInput.Contains("/artist/") => CrunchyUrlType.Artist,
|
||||
_ when UrlInput.Contains("/watch/musicvideo/") => CrunchyUrlType.MusicVideo,
|
||||
_ when UrlInput.Contains("/watch/concert/") => CrunchyUrlType.Concert,
|
||||
_ when UrlInput.Contains("/watch/") => CrunchyUrlType.Episode,
|
||||
_ when UrlInput.Contains("/series/") => CrunchyUrlType.Series,
|
||||
_ => CrunchyUrlType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task HandleArtistUrlAsync(string locale, string id){
|
||||
|
|
@ -434,88 +336,10 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
ResetState();
|
||||
}
|
||||
|
||||
private async Task HandleEpisodeUrlAsync(string locale, string id){
|
||||
var resolvedLocale = DetermineLocale(locale);
|
||||
if (SingleEpisodeInstantAdd){
|
||||
_ = CrunchyrollManager.Instance.CrQueue.CrAddEpisodeToQueue(
|
||||
id, resolvedLocale,
|
||||
CrunchyrollManager.Instance.CrunOptions.DubLang, AddSearchResultsToHistory);
|
||||
ResetState();
|
||||
return;
|
||||
}
|
||||
|
||||
await ShowSingleEpisodeForSelectionAsync(id, resolvedLocale);
|
||||
}
|
||||
|
||||
private async Task ShowSingleEpisodeForSelectionAsync(string id, string locale){
|
||||
SetLoadingState(true);
|
||||
|
||||
var episode = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(id, locale);
|
||||
CrunchyRollEpisodeData? episodeData = null;
|
||||
if (episode != null){
|
||||
episodeData = await CrunchyrollManager.Instance.CrEpisode.EpisodeData(episode, AddSearchResultsToHistory);
|
||||
}
|
||||
|
||||
SetLoadingState(false);
|
||||
|
||||
if (episode == null || episodeData?.EpisodeAndLanguages.Variants.Count == 0){
|
||||
MessageBus.Current.SendMessage(new ToastMessage("Failed to get episode", ToastType.Error, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
currentSingleEpisodeId = id;
|
||||
currentSingleEpisodeLocale = locale;
|
||||
currentSeriesList = null;
|
||||
currentMusicVideoList = null;
|
||||
Items.Clear();
|
||||
SelectedItems.Clear();
|
||||
selectedEpisodes.Clear();
|
||||
SeasonList.Clear();
|
||||
episodesBySeason.Clear();
|
||||
|
||||
var baseEpisode = episodeData.EpisodeAndLanguages.Variants.First().Item;
|
||||
var availableAudios = episodeData.EpisodeAndLanguages.Variants
|
||||
.Select(variant => variant.Lang.CrLocale)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var season = "S" + baseEpisode.GetSeasonNum();
|
||||
var episodeNumber = episodeData.Key.StartsWith("E") || episodeData.Key.StartsWith("SP")
|
||||
? episodeData.Key
|
||||
: "E" + episodeData.Key;
|
||||
var duration = TimeSpan.FromMilliseconds(baseEpisode.DurationMs);
|
||||
var itemModel = new ItemModel(
|
||||
baseEpisode.Id,
|
||||
baseEpisode.GetImageUrl(),
|
||||
baseEpisode.Description,
|
||||
$"{(int)duration.TotalMinutes}:{duration.Seconds:D2}",
|
||||
baseEpisode.GetEpisodeTitle(),
|
||||
season,
|
||||
episodeNumber,
|
||||
episodeData.Key.StartsWith('E') ? episodeData.Key[1..] : episodeData.Key,
|
||||
availableAudios,
|
||||
baseEpisode.EpisodeType);
|
||||
|
||||
episodesBySeason[season] = [itemModel];
|
||||
SeasonList.Add(new ComboBoxItem{ Content = season });
|
||||
CurrentSelectedSeason = SeasonList.First();
|
||||
AllButtonEnabled = false;
|
||||
AddAllEpisodes = false;
|
||||
SearchVisible = false;
|
||||
SlectSeasonVisible = false;
|
||||
ButtonText = "Select Episode";
|
||||
ButtonEnabled = false;
|
||||
}
|
||||
|
||||
private async Task AddSelectedSingleEpisodeToQueue(){
|
||||
if (AddAllEpisodes || SelectedItems.Count > 0 || selectedEpisodes.Count > 0){
|
||||
await CrunchyrollManager.Instance.CrQueue.CrAddEpisodeToQueue(
|
||||
currentSingleEpisodeId,
|
||||
currentSingleEpisodeLocale,
|
||||
CrunchyrollManager.Instance.CrunOptions.DubLang,
|
||||
AddSearchResultsToHistory);
|
||||
}
|
||||
|
||||
private void HandleEpisodeUrl(string locale, string id){
|
||||
_ = CrunchyrollManager.Instance.CrQueue.CrAddEpisodeToQueue(
|
||||
id, DetermineLocale(locale),
|
||||
CrunchyrollManager.Instance.CrunOptions.DubLang, true);
|
||||
ResetState();
|
||||
}
|
||||
|
||||
|
|
@ -524,7 +348,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
|
||||
var list = await CrunchyrollManager.Instance.CrSeries.ListSeriesId(
|
||||
id, DetermineLocale(locale),
|
||||
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true, AddSearchResultsToHistory);
|
||||
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true);
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.SearchFetchFeaturedMusic){
|
||||
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(id, DetermineLocale(locale), true);
|
||||
|
|
@ -585,10 +409,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
private void PopulateEpisodesBySeason(){
|
||||
var seasonKeys = BuildSeasonGroupKeys(currentSeriesList?.List ?? Enumerable.Empty<Episode>());
|
||||
|
||||
foreach (var episode in currentSeriesList?.List ?? Enumerable.Empty<Episode>()){
|
||||
var seasonKey = seasonKeys[episode];
|
||||
var seasonKey = "S" + episode.Season;
|
||||
var itemModel = new ItemModel(
|
||||
episode.Id, episode.Img, episode.Description, episode.Time, episode.Name, seasonKey,
|
||||
episode.EpisodeNum.StartsWith("SP") ? episode.EpisodeNum : "E" + episode.EpisodeNum,
|
||||
|
|
@ -607,28 +429,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
private static Dictionary<Episode, string> BuildSeasonGroupKeys(IEnumerable<Episode> episodes){
|
||||
var episodeList = episodes.ToList();
|
||||
var seasonIdIndexesBySeason = episodeList
|
||||
.GroupBy(episode => episode.Season)
|
||||
.ToDictionary(
|
||||
seasonGroup => seasonGroup.Key,
|
||||
seasonGroup => seasonGroup
|
||||
.Select(episode => episode.Id)
|
||||
.Distinct()
|
||||
.Select((seasonId, index) => new{ seasonId, index })
|
||||
.ToDictionary(item => item.seasonId, item => item.index + 1));
|
||||
|
||||
return episodeList.ToDictionary(episode => episode, episode => {
|
||||
var baseSeasonKey = "S" + episode.Season;
|
||||
var seasonIdIndexes = seasonIdIndexesBySeason[episode.Season];
|
||||
|
||||
return seasonIdIndexes.Count > 1
|
||||
? $"{baseSeasonKey} ({seasonIdIndexes[episode.Id]})"
|
||||
: baseSeasonKey;
|
||||
});
|
||||
}
|
||||
|
||||
private string DetermineLocale(string locale){
|
||||
return string.IsNullOrEmpty(locale)
|
||||
? (string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang)
|
||||
|
|
@ -755,7 +555,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
SearchVisible = true;
|
||||
SlectSeasonVisible = false;
|
||||
ShowLoading = false;
|
||||
ButtonText = SearchEnabled ? "Select Searched Series" : "Enter Url";
|
||||
SearchEnabled = false; // disable and enable for button text
|
||||
SearchEnabled = true;
|
||||
}
|
||||
|
||||
private void UpdateUiForSearchSelection(){
|
||||
|
|
@ -776,7 +577,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
return await CrunchyrollManager.Instance.CrSeries.ListSeriesId(
|
||||
seriesId,
|
||||
locale,
|
||||
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true, AddSearchResultsToHistory);
|
||||
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true);
|
||||
}
|
||||
|
||||
private async Task SearchPopulateEpisodesBySeason(string seriesId){
|
||||
|
|
@ -793,10 +594,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
|
|||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect();
|
||||
|
||||
var seasonKeys = BuildSeasonGroupKeys(currentSeriesList.List);
|
||||
|
||||
foreach (var episode in currentSeriesList.List){
|
||||
var seasonKey = seasonKeys[episode];
|
||||
var seasonKey = "S" + episode.Season;
|
||||
var episodeModel = new ItemModel(
|
||||
episode.Id,
|
||||
episode.Img,
|
||||
|
|
@ -918,4 +717,4 @@ public class ItemModel(string id, string imageUrl, string description, string ti
|
|||
ImageBitmap = await Helpers.LoadImage(url, 208, 117);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,9 +35,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool _updateHistoryFromCalendar;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showHistoryMark;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hideDubs;
|
||||
|
||||
|
|
@ -81,7 +78,6 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
|
||||
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
|
||||
UpdateHistoryFromCalendar = CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar;
|
||||
ShowHistoryMark = CrunchyrollManager.Instance.CrunOptions.CalendarShowHistoryMark;
|
||||
|
||||
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
|
||||
CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0];
|
||||
|
|
@ -306,29 +302,5 @@ public partial class CalendarPageViewModel : ViewModelBase{
|
|||
CrunchyrollManager.Instance.CrunOptions.UpdateHistoryFromCalendar = value;
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
|
||||
partial void OnShowHistoryMarkChanged(bool value){
|
||||
if (loading){
|
||||
return;
|
||||
}
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.CalendarShowHistoryMark = value;
|
||||
CfgManager.WriteCrSettings();
|
||||
RefreshVisibleHistoryMarks(value);
|
||||
}
|
||||
|
||||
private void RefreshVisibleHistoryMarks(bool value){
|
||||
foreach (var calendarEpisode in CalendarDays.SelectMany(day => day.CalendarEpisodes)){
|
||||
SetHistoryMarkVisibility(calendarEpisode, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetHistoryMarkVisibility(CalendarEpisode calendarEpisode, bool value){
|
||||
calendarEpisode.ShowHistoryMark = value;
|
||||
|
||||
foreach (var childEpisode in calendarEpisode.CalendarEpisodes){
|
||||
SetHistoryMarkVisibility(childEpisode, value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -279,7 +279,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
|
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ShowPauseIcon)));
|
||||
|
||||
QueueManager.Instance.ReleaseDownloadSlot(epMeta);
|
||||
QueueManager.Instance.NotifyQueueItemStateChanged(epMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
|
|||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadSpeed)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DoingWhat)));
|
||||
|
||||
QueueManager.Instance.NotifyQueueItemStateChanged(epMeta);
|
||||
QueueManager.Instance.RefreshQueue();
|
||||
StartDownload();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -285,15 +285,8 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public async Task RefreshSonarrEpisodeMatch(){
|
||||
SelectedSeries.SetFetchingData();
|
||||
await Task.Yield();
|
||||
|
||||
try{
|
||||
await Task.Run(async () => await CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries));
|
||||
CfgManager.UpdateHistoryFile();
|
||||
} finally{
|
||||
SelectedSeries.SetFetchingData(false);
|
||||
}
|
||||
await CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -379,4 +372,4 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
_ => Symbol.ClosedCaption
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ public partial class UpdateViewModel : ViewModelBase{
|
|||
private int textSize = 16;
|
||||
|
||||
public void LoadChangelog(){
|
||||
string changelogPath = Path.Combine(AppContext.BaseDirectory, "CHANGELOG.md");;
|
||||
string changelogPath = "CHANGELOG.md";
|
||||
|
||||
if (!File.Exists(changelogPath)){
|
||||
ChangelogBlocks.Clear();
|
||||
|
|
|
|||
|
|
@ -46,9 +46,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool historyCountMissing;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool historyCheckPartialDownloads;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool historyIncludeCrArtists;
|
||||
|
||||
|
|
@ -97,9 +94,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool downloadOnlyWithAllSelectedDubSub;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool replaceExistingFiles;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool downloadAllowEarlyStart;
|
||||
|
||||
|
|
@ -257,9 +251,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private bool proxyEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool proxyAllTraffic;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool proxySocks;
|
||||
|
||||
|
|
@ -442,14 +433,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
ProxyEnabled = options.ProxyEnabled;
|
||||
ProxyAllTraffic = options.ProxyAllTraffic;
|
||||
ProxySocks = options.ProxySocks;
|
||||
ProxyHost = options.ProxyHost ?? "";
|
||||
ProxyUsername = options.ProxyUsername ?? "";
|
||||
ProxyPassword = options.ProxyPassword ?? "";
|
||||
ProxyPort = options.ProxyPort;
|
||||
HistoryCountMissing = options.HistoryCountMissing;
|
||||
HistoryCheckPartialDownloads = options.HistoryCheckPartialDownloads;
|
||||
HistoryIncludeCrArtists = options.HistoryIncludeCrArtists;
|
||||
HistoryRemoveMissingEpisodes = options.HistoryRemoveMissingEpisodes;
|
||||
HistoryAddSpecials = options.HistoryAddSpecials;
|
||||
|
|
@ -464,7 +453,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
DownloadMethodeNew = options.DownloadMethodeNew;
|
||||
DownloadAllowEarlyStart = options.DownloadAllowEarlyStart;
|
||||
DownloadOnlyWithAllSelectedDubSub = options.DownloadOnlyWithAllSelectedDubSub;
|
||||
ReplaceExistingFiles = options.ReplaceExistingFiles;
|
||||
PersistQueue = options.PersistQueue;
|
||||
RetryAttempts = Math.Clamp((options.RetryAttempts), 1, 10);
|
||||
RetryDelay = Math.Clamp((options.RetryDelay), 1, 30);
|
||||
|
|
@ -542,7 +530,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
settings.DownloadMethodeNew = DownloadMethodeNew;
|
||||
settings.DownloadAllowEarlyStart = DownloadAllowEarlyStart;
|
||||
settings.DownloadOnlyWithAllSelectedDubSub = DownloadOnlyWithAllSelectedDubSub;
|
||||
settings.ReplaceExistingFiles = ReplaceExistingFiles;
|
||||
settings.PersistQueue = PersistQueue;
|
||||
|
||||
settings.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
|
||||
|
|
@ -555,7 +542,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
settings.DownloadToTempFolder = DownloadToTempFolder;
|
||||
settings.HistoryCountMissing = HistoryCountMissing;
|
||||
settings.HistoryCheckPartialDownloads = HistoryCheckPartialDownloads;
|
||||
settings.HistoryAddSpecials = HistoryAddSpecials;
|
||||
settings.HistoryIncludeCrArtists = HistoryIncludeCrArtists;
|
||||
settings.HistoryRemoveMissingEpisodes = HistoryRemoveMissingEpisodes;
|
||||
|
|
@ -572,7 +558,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
QueueManager.Instance.SetProcessingLimit(settings.SimultaneousProcessingJobs);
|
||||
|
||||
settings.ProxyEnabled = ProxyEnabled;
|
||||
settings.ProxyAllTraffic = ProxyAllTraffic;
|
||||
settings.ProxySocks = ProxySocks;
|
||||
settings.ProxyHost = ProxyHost;
|
||||
settings.ProxyPort = Math.Clamp((int)(ProxyPort ?? 0), 0, 65535);
|
||||
|
|
@ -1219,6 +1204,10 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
Parallel.ForEach(historyList, historySeries => {
|
||||
historySeries.Init();
|
||||
|
||||
foreach (var historySeriesSeason in historySeries.Seasons){
|
||||
historySeriesSeason.Init();
|
||||
}
|
||||
});
|
||||
} else{
|
||||
CrunchyrollManager.Instance.HistoryList = new ObservableCollection<HistorySeries>();
|
||||
|
|
@ -1233,15 +1222,6 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
if (settingsLoaded && e.PropertyName is nameof(HistoryCheckPartialDownloads)){
|
||||
foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){
|
||||
historySeries.UpdateNewEpisodes();
|
||||
foreach (var episode in historySeries.Seasons.SelectMany(season => season.EpisodesList)){
|
||||
episode.RefreshDownloadState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(BackgroundImagePath) && e.PropertyName is nameof(BackgroundImageBlurRadius) or nameof(BackgroundImageOpacity)){
|
||||
Helpers.SetBackgroundImage(BackgroundImagePath, BackgroundImageOpacity, BackgroundImageBlurRadius);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,17 +30,8 @@
|
|||
|
||||
<StackPanel Grid.Row="0" Orientation="Vertical">
|
||||
|
||||
<Grid Margin="10" ColumnDefinitions="*,Auto">
|
||||
<TextBox x:Name="SearchBar" Grid.Column="0" Watermark="{Binding SearchWatermark}" Text="{Binding UrlInput}"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<ToggleButton x:Name="AddDownloadSettings" Grid.Column="1" Margin="8 0 0 0" HorizontalContentAlignment="Stretch">
|
||||
<controls:SymbolIcon Symbol="Settings" FontSize="18" />
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Add download settings" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
<TextBox x:Name="SearchBar" Watermark="Enter series or episode url" Text="{Binding UrlInput}" Margin="10"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<Popup IsLightDismissEnabled="True"
|
||||
MaxWidth="{Binding Bounds.Width, ElementName=SearchBar}"
|
||||
|
|
@ -125,81 +116,6 @@
|
|||
</Border>
|
||||
</Popup>
|
||||
|
||||
<Popup IsLightDismissEnabled="True"
|
||||
MaxWidth="360"
|
||||
MaxHeight="600"
|
||||
IsOpen="{Binding IsChecked, ElementName=AddDownloadSettings, Mode=TwoWay}"
|
||||
Placement="Bottom"
|
||||
PlacementTarget="{Binding ElementName=AddDownloadSettings}">
|
||||
<Border BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Padding="12"
|
||||
Background="{DynamicResource ComboBoxDropDownBackground}">
|
||||
<StackPanel Width="320" Spacing="8">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Add searched series to history" VerticalAlignment="Center" />
|
||||
<controls:SymbolIcon Grid.Column="1"
|
||||
Symbol="Help"
|
||||
FontSize="14"
|
||||
Opacity="0.72"
|
||||
Margin="8 0 10 0"
|
||||
VerticalAlignment="Center">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Listing episodes from this page updates History when global History is enabled."
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="280" />
|
||||
</ToolTip.Tip>
|
||||
</controls:SymbolIcon>
|
||||
<CheckBox Grid.Column="2"
|
||||
IsChecked="{Binding AddSearchResultsToHistory}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Single episode URLs" VerticalAlignment="Center" />
|
||||
<controls:SymbolIcon Grid.Column="1"
|
||||
Symbol="Help"
|
||||
FontSize="14"
|
||||
Opacity="0.72"
|
||||
Margin="8 0 10 0"
|
||||
VerticalAlignment="Center">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Add episode URLs directly, or disable this to show a selectable episode card first."
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="280" />
|
||||
</ToolTip.Tip>
|
||||
</controls:SymbolIcon>
|
||||
<CheckBox Grid.Column="2"
|
||||
IsChecked="{Binding SingleEpisodeInstantAdd}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Default search mode" VerticalAlignment="Center" />
|
||||
<controls:SymbolIcon Grid.Column="1"
|
||||
Symbol="Help"
|
||||
FontSize="14"
|
||||
Opacity="0.72"
|
||||
Margin="8 0 10 0"
|
||||
VerticalAlignment="Center">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Open this page in title search mode instead of URL mode."
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="280" />
|
||||
</ToolTip.Tip>
|
||||
</controls:SymbolIcon>
|
||||
<CheckBox Grid.Column="2"
|
||||
IsChecked="{Binding DefaultSearchEnabled}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
|
|
@ -317,4 +233,4 @@
|
|||
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
|
@ -103,9 +103,6 @@
|
|||
<CheckBox IsChecked="{Binding UpdateHistoryFromCalendar}"
|
||||
Content="Update History from Calendar" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowHistoryMark}"
|
||||
Content="Show History mark" Margin="5 5 0 0">
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
</controls:SettingsExpander.Footer>
|
||||
|
|
@ -229,18 +226,6 @@
|
|||
</Viewbox>
|
||||
</Canvas>
|
||||
</StackPanel>
|
||||
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Left"
|
||||
IsVisible="{Binding IsHistoryMarkVisible}" Margin="5,0,0,5">
|
||||
<Grid>
|
||||
<Ellipse Width="25" Height="25"
|
||||
Fill="{Binding HistoryDownloadStatusBrush}" />
|
||||
<controls:SymbolIcon Symbol="Checkmark"
|
||||
FontSize="18" />
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="{Binding HistoryDownloadStatusTooltip}" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock HorizontalAlignment="Center" TextAlignment="Center" Text="{Binding SeasonName}"
|
||||
|
|
@ -271,4 +256,4 @@
|
|||
</Grid>
|
||||
|
||||
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
|
@ -20,7 +20,6 @@
|
|||
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
|
||||
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" />
|
||||
<ui:UiEnumToBoolConverter x:Key="EnumToBoolConverter" />
|
||||
<ui:UiHistoryDownloadStatusConverter x:Key="UiHistoryDownloadStatusConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
|
|
@ -95,8 +94,7 @@
|
|||
<Popup IsLightDismissEnabled="True"
|
||||
IsOpen="{Binding IsSearchOpen, Mode=TwoWay}"
|
||||
Placement="BottomEdgeAlignedRight"
|
||||
PlacementTarget="{Binding ElementName=DropdownButtonSearch}"
|
||||
Opened="SearchPopup_OnOpened">
|
||||
PlacementTarget="{Binding ElementName=DropdownButtonSearch}">
|
||||
|
||||
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
|
||||
<StackPanel Orientation="Horizontal" Margin="10">
|
||||
|
|
@ -625,7 +623,7 @@
|
|||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem, FallbackValue=''}"
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem.stringValue, FallbackValue=''}"
|
||||
VerticalAlignment="Center" />
|
||||
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
|
||||
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
|
||||
|
|
@ -640,7 +638,13 @@
|
|||
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding VideoQualityList , Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedVideoQualityItem}" />
|
||||
SelectedItem="{Binding SelectedVideoQualityItem}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -673,7 +677,13 @@
|
|||
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding DubLangList, Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedDubLang}" />
|
||||
SelectedItems="{Binding SelectedDubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -703,7 +713,13 @@
|
|||
<ListBox SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding SubLangList, Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedSubLang}" />
|
||||
SelectedItems="{Binding SelectedSubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -786,27 +802,8 @@
|
|||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Background="Transparent"
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<ToolTip.Tip>
|
||||
<Grid ColumnDefinitions="Auto,*" MaxWidth="600">
|
||||
<TextBlock Grid.Column="0" Text="Subs: " />
|
||||
<ui:HighlightingTextBlock
|
||||
Grid.Column="1"
|
||||
Items="{Binding HistoryEpisodeAvailableSoftSubs}"
|
||||
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
|
||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="False"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
FontStyle="Italic"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</ToolTip.Tip>
|
||||
<TextBlock FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8" Text="Dubs: ">
|
||||
|
|
@ -871,47 +868,11 @@
|
|||
Command="{Binding ToggleWasDownloadedSeries}"
|
||||
CommandParameter="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}">
|
||||
<Grid>
|
||||
<Ellipse Width="25" Height="25">
|
||||
<Ellipse.Fill>
|
||||
<MultiBinding Converter="{StaticResource UiHistoryDownloadStatusConverter}">
|
||||
<Binding Path="." />
|
||||
<Binding Path="$parent[ScrollViewer].((history:HistorySeries)DataContext)" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext)" />
|
||||
<Binding Path="WasDownloaded" />
|
||||
<Binding Path="DownloadedDubLang" />
|
||||
<Binding Path="DownloadedSoftSubs" />
|
||||
<Binding Path="HistoryEpisodeAvailableDubLang" />
|
||||
<Binding Path="HistoryEpisodeAvailableSoftSubs" />
|
||||
<Binding Path="$parent[ScrollViewer].((history:HistorySeries)DataContext).SelectedDubs" />
|
||||
<Binding Path="$parent[ScrollViewer].((history:HistorySeries)DataContext).SelectedSubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedDubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedSubs" />
|
||||
</MultiBinding>
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
<Ellipse Width="25" Height="25"
|
||||
Fill="#21a556" />
|
||||
<controls:SymbolIcon Symbol="Checkmark"
|
||||
FontSize="18" />
|
||||
</Grid>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock FontSize="15">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource UiHistoryDownloadStatusConverter}" ConverterParameter="Tooltip">
|
||||
<Binding Path="." />
|
||||
<Binding Path="$parent[ScrollViewer].((history:HistorySeries)DataContext)" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext)" />
|
||||
<Binding Path="WasDownloaded" />
|
||||
<Binding Path="DownloadedDubLang" />
|
||||
<Binding Path="DownloadedSoftSubs" />
|
||||
<Binding Path="HistoryEpisodeAvailableDubLang" />
|
||||
<Binding Path="HistoryEpisodeAvailableSoftSubs" />
|
||||
<Binding Path="$parent[ScrollViewer].((history:HistorySeries)DataContext).SelectedDubs" />
|
||||
<Binding Path="$parent[ScrollViewer].((history:HistorySeries)DataContext).SelectedSubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedDubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedSubs" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</ToolTip.Tip>
|
||||
</Button>
|
||||
|
||||
<Button Margin="0 0 5 0" FontStyle="Italic"
|
||||
|
|
@ -1142,7 +1103,7 @@
|
|||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem, FallbackValue=''}"
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem.stringValue, FallbackValue=''}"
|
||||
VerticalAlignment="Center" />
|
||||
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
|
||||
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
|
||||
|
|
@ -1157,7 +1118,13 @@
|
|||
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding VideoQualityList , Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedVideoQualityItem}" />
|
||||
SelectedItem="{Binding SelectedVideoQualityItem}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -1190,7 +1157,13 @@
|
|||
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding DubLangList}"
|
||||
SelectedItems="{Binding SelectedDubLang}" />
|
||||
SelectedItems="{Binding SelectedDubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -1220,7 +1193,13 @@
|
|||
<ListBox SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding SubLangList}"
|
||||
SelectedItems="{Binding SelectedSubLang}" />
|
||||
SelectedItems="{Binding SelectedSubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using CRD.ViewModels;
|
||||
|
||||
namespace CRD.Views;
|
||||
|
|
@ -25,11 +23,4 @@ public partial class HistoryPageView : UserControl{
|
|||
if (SeriesListBox.Scroll != null) SeriesListBox.Scroll.Offset = viewModel.LastScrollOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchPopup_OnOpened(object? sender, EventArgs e){
|
||||
Dispatcher.UIThread.Post(() => {
|
||||
SearchBar.Focus();
|
||||
SearchBar.SelectAll();
|
||||
}, DispatcherPriority.Input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
<ui:UiEmptyToDefaultConverter x:Key="EmptyToDefault" />
|
||||
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
|
||||
<ui:UiEnumToBoolConverter x:Key="EnumToBoolConverter" />
|
||||
<ui:UiHistoryDownloadStatusConverter x:Key="UiHistoryDownloadStatusConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
|
|
@ -236,7 +235,7 @@
|
|||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem, FallbackValue=''}"
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem.stringValue, FallbackValue=''}"
|
||||
VerticalAlignment="Center" />
|
||||
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
|
||||
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
|
||||
|
|
@ -248,10 +247,16 @@
|
|||
Placement="Bottom"
|
||||
PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}">
|
||||
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
|
||||
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
|
||||
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding SelectedSeries.VideoQualityList , Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}" />
|
||||
SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -284,7 +289,13 @@
|
|||
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding SelectedSeries.DubLangList , Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedSeries.SelectedDubLang}" />
|
||||
SelectedItems="{Binding SelectedSeries.SelectedDubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -314,7 +325,13 @@
|
|||
<ListBox SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding SelectedSeries.SubLangList , Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedSeries.SelectedSubLang}" />
|
||||
SelectedItems="{Binding SelectedSeries.SelectedSubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -426,27 +443,8 @@
|
|||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Background="Transparent"
|
||||
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<ToolTip.Tip>
|
||||
<Grid ColumnDefinitions="Auto,*" MaxWidth="600">
|
||||
<TextBlock Grid.Column="0" Text="Subs: " />
|
||||
<ui:HighlightingTextBlock
|
||||
Grid.Column="1"
|
||||
Items="{Binding HistoryEpisodeAvailableSoftSubs}"
|
||||
Series="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries}"
|
||||
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
|
||||
StreamingService="Crunchyroll"
|
||||
CheckDubs="False"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
FontStyle="Italic"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</ToolTip.Tip>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8" Text="Dubs: ">
|
||||
|
|
@ -464,6 +462,15 @@
|
|||
|
||||
|
||||
</StackPanel>
|
||||
<!-- <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> -->
|
||||
<!-- <TextBlock FontStyle="Italic" -->
|
||||
<!-- FontSize="12" -->
|
||||
<!-- Opacity="0.8" Text="Subs: "> -->
|
||||
<!-- </TextBlock> -->
|
||||
<!-- <TextBlock FontStyle="Italic" -->
|
||||
<!-- FontSize="12" -->
|
||||
<!-- Opacity="0.8" Text="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListToStringConverter}}" /> -->
|
||||
<!-- </StackPanel> -->
|
||||
</StackPanel>
|
||||
|
||||
|
||||
|
|
@ -536,46 +543,9 @@
|
|||
Command="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext).UpdateDownloaded}"
|
||||
CommandParameter="{Binding EpisodeId}">
|
||||
<Grid>
|
||||
<Ellipse Width="25" Height="25">
|
||||
<Ellipse.Fill>
|
||||
<MultiBinding Converter="{StaticResource UiHistoryDownloadStatusConverter}">
|
||||
<Binding Path="." />
|
||||
<Binding Path="$parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext)" />
|
||||
<Binding Path="WasDownloaded" />
|
||||
<Binding Path="DownloadedDubLang" />
|
||||
<Binding Path="DownloadedSoftSubs" />
|
||||
<Binding Path="HistoryEpisodeAvailableDubLang" />
|
||||
<Binding Path="HistoryEpisodeAvailableSoftSubs" />
|
||||
<Binding Path="$parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries.SelectedDubs" />
|
||||
<Binding Path="$parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries.SelectedSubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedDubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedSubs" />
|
||||
</MultiBinding>
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
<Ellipse Width="25" Height="25" Fill="#21a556" />
|
||||
<controls:SymbolIcon Symbol="Checkmark" FontSize="18" />
|
||||
</Grid>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock FontSize="15">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource UiHistoryDownloadStatusConverter}" ConverterParameter="Tooltip">
|
||||
<Binding Path="." />
|
||||
<Binding Path="$parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext)" />
|
||||
<Binding Path="WasDownloaded" />
|
||||
<Binding Path="DownloadedDubLang" />
|
||||
<Binding Path="DownloadedSoftSubs" />
|
||||
<Binding Path="HistoryEpisodeAvailableDubLang" />
|
||||
<Binding Path="HistoryEpisodeAvailableSoftSubs" />
|
||||
<Binding Path="$parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries.SelectedDubs" />
|
||||
<Binding Path="$parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedSeries.SelectedSubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedDubs" />
|
||||
<Binding Path="$parent[controls:SettingsExpander].((history:HistorySeason)DataContext).SelectedSubs" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</ToolTip.Tip>
|
||||
</Button>
|
||||
|
||||
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
|
||||
|
|
@ -798,7 +768,7 @@
|
|||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem, FallbackValue=''}"
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem.stringValue, FallbackValue=''}"
|
||||
VerticalAlignment="Center" />
|
||||
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
|
||||
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
|
||||
|
|
@ -813,7 +783,13 @@
|
|||
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding VideoQualityList , Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedVideoQualityItem}" />
|
||||
SelectedItem="{Binding SelectedVideoQualityItem}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -846,7 +822,13 @@
|
|||
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding DubLangList , Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedDubLang}" />
|
||||
SelectedItems="{Binding SelectedDubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -876,7 +858,13 @@
|
|||
<ListBox SelectionMode="Multiple,Toggle" Width="210"
|
||||
MaxHeight="400"
|
||||
ItemsSource="{Binding SubLangList , Mode=OneWay}"
|
||||
SelectedItems="{Binding SelectedSubLang}" />
|
||||
SelectedItems="{Binding SelectedSubLang}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type structs:StringItem}">
|
||||
<TextBlock Text="{Binding stringValue}"></TextBlock>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
|
@ -931,4 +919,4 @@
|
|||
<controls:ProgressRing Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using System.Threading.Tasks;
|
||||
using CRD.Downloader;
|
||||
using CRD.Utils.Sonarr;
|
||||
using CRD.ViewModels;
|
||||
|
|
@ -13,10 +12,10 @@ public partial class SettingsPageView : UserControl{
|
|||
}
|
||||
|
||||
private void OnUnloaded(object? sender, RoutedEventArgs e){
|
||||
if (DataContext is SettingsPageViewModel){
|
||||
_ = Task.Run(SonarrClient.Instance.RefreshSonarr);
|
||||
if (DataContext is SettingsPageViewModel viewModel){
|
||||
SonarrClient.Instance.RefreshSonarr();
|
||||
ProgramManager.Instance.StartRunners();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -39,12 +39,6 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="History Check Partial Downloads" Description="Downloaded episodes missing selected dubs/subs are marked orange and counted again when the missing media becomes available">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding HistoryCheckPartialDownloads}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="History Include CR Artists" Description="Add Crunchyroll artists (music) to the history">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding HistoryIncludeCrArtists}"> </CheckBox>
|
||||
|
|
@ -150,12 +144,6 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Replace Existing Files" Description="When enabled, downloads overwrite an existing final output file instead of creating a numbered copy">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding ReplaceExistingFiles}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Max Download Speed"
|
||||
Description="Download in Kb/s - 0 is full speed">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
|
|
@ -552,12 +540,6 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Proxy All Traffic">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding ProxyAllTraffic}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use Socks5 Proxy">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding ProxySocks}"> </CheckBox>
|
||||
|
|
|
|||
Loading…
Reference in a new issue