mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-09-12 21:57:19 -04:00
'Not Downloaded' reads as a claim about the file on disk, but the status it names is really an instruction about the future. Telling someone with a finished audiobook to set it 'Not Downloaded' asks them to assert something they know is false, which is why the question keeps coming up. 'Download Pending' says the same thing about intent without saying anything untrue about the file, and stands alone in a dropdown or a support reply where bare 'Pending' would not. The context menu carriers move from "Set Download status to 'X'" to "Mark as 'X'" so the word does not land twice in one breath, with the accelerator on P to stay clear of Downloaded's D. The persisted enum, the --not-downloaded CLI flag and the IsLiberated search tags are unchanged, so scripts and saved quick filters keep working. WinForms status combo boxes grow from 121 to 150px and the better-quality Mark button from 210 to 240px to fit the longer label. Docs carry 'previously "Not Downloaded"' on first mention, since years of Reddit and GitHub answers use the old name. Co-authored-by: rmcrackan <rmcrackan@gmail.com>
218 lines
9.0 KiB
C#
218 lines
9.0 KiB
C#
using ApplicationServices;
|
|
using DataLayer;
|
|
using Dinah.Core;
|
|
using FileLiberator;
|
|
using LibationFileManager;
|
|
using LibationFileManager.Templates;
|
|
using LibationUiBase.Forms;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LibationUiBase.GridView;
|
|
|
|
public delegate void LiberateClickedHandler(object? sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config);
|
|
public class GridContextMenu
|
|
{
|
|
public string CopyCellText => $"{Accelerator}Copy Cell Contents";
|
|
public string LiberateEpisodesText => $"{Accelerator}Liberate All Episodes";
|
|
public string SetDownloadedText => $"Mark as '{Accelerator}Downloaded'";
|
|
public string SetNotDownloadedText => $"Mark as 'Download {Accelerator}Pending'";
|
|
public string SetPdfDownloadedText => "Mark PDF as 'Downloaded'";
|
|
public string SetPdfNotDownloadedText => "Mark PDF as 'Download Pending'";
|
|
public string RemoveText => $"{Accelerator}Remove from library";
|
|
public string RemoveFromAudibleText => $"Remove Plus {(GridEntries.Count(e => e.LibraryBook.IsAudiblePlus) == 1 ? "Book" : "Books")} from Audible Library";
|
|
public string LocateFileText => $"{Accelerator}Locate file...";
|
|
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book?.TitleWithSubtitle ?? "[null]"}'";
|
|
public string LocateFileErrorMessage => "Error saving book's location";
|
|
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
|
|
public string DownloadAsChapters => $"Download {Accelerator}split by chapters";
|
|
public string ReDownloadText => "Re-download this audiobook";
|
|
public string DownloadSingleText => "Download this audiobook";
|
|
public string DownloadSelectedText => "Download selected audiobooks";
|
|
public string EditTemplatesText => "Edit Templates";
|
|
public string FolderTemplateText => "Folder Template";
|
|
public string FileTemplateText => "File Template";
|
|
public string MultipartTemplateText => "Multipart File Template";
|
|
public string ViewBookmarksText => $"View {Accelerator}Bookmarks/Clips";
|
|
public string ViewSeriesText => GridEntries[0].Liberate?.IsSeries is true ? "View All Episodes in Series" : "View All Books in Series";
|
|
|
|
public bool LiberateEpisodesEnabled => GridEntries.OfType<SeriesEntry>().Any(sEntry => sEntry.Children.Any(c => c.Liberate?.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload));
|
|
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate?.IsSeries is true);
|
|
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate?.IsSeries is true);
|
|
|
|
/// <summary>
|
|
/// Whether to offer the PDF status on its own. The pair above deliberately moves both statuses together,
|
|
/// so resetting a title's PDF from here also queues its audiobook for a fresh download. The only PDF-only
|
|
/// control was the bulk one under Visible Books, which applies to everything on screen, leaving no way to
|
|
/// say it about one title. Offered only for a selection that has a PDF, since for anything else the pair
|
|
/// above is already audio-only.
|
|
/// </summary>
|
|
public bool ShowPdfStatusItems => PdfItems.Any();
|
|
public bool SetPdfDownloadedEnabled => PdfItems.Any(udi => udi.PdfStatus != LiberatedStatus.Liberated);
|
|
public bool SetPdfNotDownloadedEnabled => PdfItems.Any(udi => udi.PdfStatus != LiberatedStatus.NotLiberated);
|
|
|
|
private IEnumerable<UserDefinedItem> PdfItems
|
|
=> LibraryBookEntries.Select(ge => ge.Book?.UserDefinedItem).OfType<UserDefinedItem>().Where(udi => udi.Book.HasPdf);
|
|
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
|
public bool DownloadBookEnabled => LibraryBookEntries.Any(ge => ge.LibraryBook.NeedsBookDownload || ge.LibraryBook.NeedsPdfDownload);
|
|
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
|
|
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
|
public bool RemoveFromAudibleEnabled => LibraryBookEntries.Any(ge => ge.LibraryBook.IsAudiblePlus);
|
|
|
|
private GridEntry[] GridEntries { get; }
|
|
public LibraryBookEntry[] LibraryBookEntries { get; }
|
|
public char Accelerator { get; }
|
|
|
|
public GridContextMenu(GridEntry[] gridEntries, char accelerator)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(gridEntries, nameof(gridEntries));
|
|
ArgumentOutOfRangeException.ThrowIfZero(gridEntries.Length, $"{nameof(gridEntries)}.{nameof(gridEntries.Length)}");
|
|
|
|
GridEntries = gridEntries;
|
|
Accelerator = accelerator;
|
|
LibraryBookEntries
|
|
= GridEntries
|
|
.OfType<SeriesEntry>()
|
|
.SelectMany(s => s.Children)
|
|
.Concat(GridEntries.OfType<LibraryBookEntry>())
|
|
.ToArray();
|
|
}
|
|
|
|
public void SetDownloaded()
|
|
{
|
|
LibraryBookEntries.Select(e => e.LibraryBook)
|
|
.UpdateUserDefinedItemAsync(udi =>
|
|
{
|
|
udi.BookStatus = LiberatedStatus.Liberated;
|
|
if (udi.Book.HasPdf)
|
|
udi.SetPdfStatus(LiberatedStatus.Liberated);
|
|
});
|
|
}
|
|
|
|
public void SetNotDownloaded()
|
|
{
|
|
LibraryBookEntries.Select(e => e.LibraryBook)
|
|
.UpdateUserDefinedItemAsync(udi =>
|
|
{
|
|
udi.BookStatus = LiberatedStatus.NotLiberated;
|
|
if (udi.Book.HasPdf)
|
|
udi.SetPdfStatus(LiberatedStatus.NotLiberated);
|
|
});
|
|
}
|
|
|
|
public void SetPdfDownloaded() => setPdfStatus(LiberatedStatus.Liberated);
|
|
|
|
public void SetPdfNotDownloaded() => setPdfStatus(LiberatedStatus.NotLiberated);
|
|
|
|
private void setPdfStatus(LiberatedStatus pdfStatus)
|
|
{
|
|
LibraryBookEntries.Select(e => e.LibraryBook)
|
|
.UpdateUserDefinedItemAsync(udi =>
|
|
{
|
|
if (udi.Book.HasPdf)
|
|
udi.SetPdfStatus(pdfStatus);
|
|
});
|
|
}
|
|
|
|
public async Task RemoveAsync()
|
|
{
|
|
await LibraryBookEntries.Select(e => e.LibraryBook).RemoveBooksAsync();
|
|
}
|
|
|
|
public async Task RemoveFromAudibleAsync()
|
|
{
|
|
LibraryBook[] toRemove = LibraryBookEntries.Select(l => l.LibraryBook).Where(lb => lb.IsAudiblePlus).ToArray();
|
|
if (toRemove.Length == 0)
|
|
return;
|
|
|
|
string bookStr = "book".PluralizeWithCount(toRemove.Length), itsThem = toRemove.Length == 1 ? "it" : "them";
|
|
string confirmMessage = $"""
|
|
Libation is about to remove {bookStr} from your Audible account. The only way to get {itsThem} back
|
|
is to re-add {itsThem} to your Audible Library through the Audible website or app.
|
|
|
|
Are you sure you want to remove the following {bookStr}?
|
|
|
|
{toRemove.AggregateTitles()}
|
|
""";
|
|
DialogResult result = await MessageBoxBase.Show(confirmMessage, "Confirm Remove from Audible Library", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
|
|
if (result != DialogResult.Yes)
|
|
return;
|
|
|
|
List<LibraryBook> removedFromAudible = [];
|
|
List<LibraryBook> failedToRemove = [];
|
|
|
|
//Getting the API loads AccountsSettings every time and es expensive
|
|
//cache Api to improve perfomanc on large batches of deletions
|
|
Dictionary<string, AudibleApi.Api> apis = [];
|
|
|
|
foreach (var entry in toRemove)
|
|
{
|
|
try
|
|
{
|
|
if (!apis.TryGetValue(entry.Account, out var api))
|
|
{
|
|
apis[entry.Account] = api = await entry.GetApiAsync();
|
|
}
|
|
|
|
bool success = await api.RemoveItemFromLibraryAsync(entry.Book.AudibleProductId);
|
|
if (success)
|
|
{
|
|
removedFromAudible.Add(entry);
|
|
}
|
|
else
|
|
{
|
|
failedToRemove.Add(entry);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Serilog.Log.Logger.Error(ex, "Failed to remove book from audible account. {@Book}", entry.LogFriendly());
|
|
failedToRemove.Add(entry);
|
|
}
|
|
}
|
|
if (failedToRemove.Count > 0)
|
|
{
|
|
string booksStr = "book".PluralizeWithCount(failedToRemove.Count);
|
|
string message = $"""
|
|
Failed to remove {booksStr} from Audible.
|
|
|
|
{failedToRemove.AggregateTitles()}
|
|
""";
|
|
await MessageBoxBase.Show(message, $"Failed to Remove {booksStr} from Audible");
|
|
}
|
|
try
|
|
{
|
|
await removedFromAudible.PermanentlyDeleteBooksAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Serilog.Log.Logger.Error(ex, "Failed to delete locally removed from Audible books.");
|
|
|
|
string booksStr = "book".PluralizeWithCount(removedFromAudible.Count);
|
|
string message = $"""
|
|
Failed to delete {booksStr} from Libation.
|
|
|
|
{removedFromAudible.AggregateTitles()}
|
|
""";
|
|
await MessageBoxBase.Show(message, $"Failed to Delete {booksStr} from Libation");
|
|
}
|
|
}
|
|
|
|
public ITemplateEditor CreateTemplateEditor<T>(LibraryBook libraryBook, string existingTemplate)
|
|
where T : Templates, ITemplate, new()
|
|
{
|
|
LibraryBookDto fileDto = libraryBook.ToDto(), folderDto = fileDto;
|
|
|
|
if (libraryBook.Book.IsEpisodeChild() &&
|
|
Configuration.Instance.SavePodcastsToParentFolder &&
|
|
libraryBook.Book.SeriesLink.SingleOrDefault() is SeriesBook series)
|
|
{
|
|
var seriesParent = DbContexts.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId, account: libraryBook.Account);
|
|
folderDto = seriesParent?.ToDto() ?? fileDto;
|
|
}
|
|
|
|
return TemplateEditor<T>.CreateFilenameEditor(Configuration.Instance.Books ?? System.IO.Path.GetTempPath(), existingTemplate, folderDto, fileDto);
|
|
}
|
|
} |