mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-07-29 16:36:00 -04:00
fix: Address review feedback - UI parity, soft-fail, token encryption, pagination, CLI wiring
This commit is contained in:
@@ -13,9 +13,9 @@ namespace ApplicationServices;
|
||||
|
||||
public static class AudiobookshelfApiService
|
||||
{
|
||||
public record Library(string Id, string Name, List<Folder> Folders);
|
||||
public record Library(string Id, string Name, string MediaType, List<Folder> Folders);
|
||||
public record Folder(string Id, string FullPath);
|
||||
public record LoginResponse(string Token);
|
||||
public enum UploadResult { Success, AlreadyExists, Failed }
|
||||
|
||||
private static HttpClient CreateClient(string serverUrl)
|
||||
{
|
||||
@@ -25,67 +25,155 @@ public static class AudiobookshelfApiService
|
||||
return client;
|
||||
}
|
||||
|
||||
public static async Task<LoginResponse?> LoginAsync(string serverUrl, string username, string password)
|
||||
{
|
||||
using var client = CreateClient(serverUrl);
|
||||
var content = new StringContent(
|
||||
$"{{\"username\":\"{JsonEscape(username)}\",\"password\":\"{JsonEscape(password)}\"}}",
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await client.PostAsync("login", content);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var obj = JObject.Parse(json);
|
||||
var token = obj["user"]?["token"]?.Value<string>();
|
||||
|
||||
return token is null ? null : new LoginResponse(token);
|
||||
}
|
||||
|
||||
public static async Task<List<Library>> GetLibrariesAsync(string serverUrl, string apiToken)
|
||||
{
|
||||
apiToken = AudiobookshelfTokenStorage.DecryptToken(apiToken) ?? "";
|
||||
using var client = CreateClient(serverUrl);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
|
||||
|
||||
var response = await client.GetAsync("api/libraries");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync();
|
||||
throw new HttpRequestException($"Audiobookshelf API returned {(int)response.StatusCode} ({response.StatusCode}) when fetching libraries. Response: {errorBody}");
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var obj = JObject.Parse(json);
|
||||
var libraries = obj["libraries"] as JArray ?? new JArray();
|
||||
|
||||
return libraries.Select(l => new Library(
|
||||
var allLibraries = libraries.Select(l => new Library(
|
||||
l["id"]?.Value<string>() ?? "",
|
||||
l["name"]?.Value<string>() ?? "",
|
||||
l["mediaType"]?.Value<string>() ?? "book",
|
||||
(l["folders"] as JArray ?? new JArray())
|
||||
.Select(f => new Folder(
|
||||
f["id"]?.Value<string>() ?? "",
|
||||
f["fullPath"]?.Value<string>() ?? ""))
|
||||
.ToList()))
|
||||
.ToList();
|
||||
|
||||
// Only return libraries with book media type
|
||||
return allLibraries.Where(l => string.Equals(l.MediaType, "book", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
public static async Task<bool> TestConnectionAsync(string serverUrl, string apiToken)
|
||||
public static async Task<bool> BookExistsAsync(string serverUrl, string apiToken, string libraryId, string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = CreateClient(serverUrl);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
apiToken = AudiobookshelfTokenStorage.DecryptToken(apiToken) ?? "";
|
||||
using var client = CreateClient(serverUrl);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
|
||||
client.Timeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
var response = await client.GetAsync("api/libraries");
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
var normalizedTitle = NormalizeTitle(title);
|
||||
var baseTitle = GetBaseTitle(normalizedTitle);
|
||||
|
||||
int page = 0;
|
||||
const int limit = 500;
|
||||
const int maxPages = 200;
|
||||
|
||||
Serilog.Log.Logger.Information("Audiobookshelf duplicate check: looking for '{Title}' (base: '{BaseTitle}') in library {LibraryId}", normalizedTitle, baseTitle, libraryId);
|
||||
|
||||
while (page < maxPages)
|
||||
{
|
||||
return false;
|
||||
var url = $"api/libraries/{Uri.EscapeDataString(libraryId)}/items?minified=1&limit={limit}&page={page}";
|
||||
var response = await client.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync();
|
||||
throw new HttpRequestException($"Audiobookshelf API returned {(int)response.StatusCode} when checking for existing books. Body: {errorBody}");
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var obj = JObject.Parse(json);
|
||||
var results = obj["results"] as JArray ?? new JArray();
|
||||
|
||||
Serilog.Log.Logger.Information("Audiobookshelf duplicate check page {Page}: received {Count} items (total={Total})", page, results.Count, obj["total"]?.Value<int>() ?? -1);
|
||||
|
||||
if (results.Count == 0)
|
||||
break;
|
||||
|
||||
foreach (var item in results)
|
||||
{
|
||||
var itemTitle = item["media"]?["metadata"]?["title"]?.Value<string>()?.Replace("\u00A0", " ").Trim();
|
||||
var itemSubtitle = item["media"]?["metadata"]?["subtitle"]?.Value<string>()?.Replace("\u00A0", " ").Trim();
|
||||
|
||||
var itemFullTitle = string.IsNullOrWhiteSpace(itemSubtitle)
|
||||
? itemTitle
|
||||
: $"{itemTitle}: {itemSubtitle}";
|
||||
|
||||
var normalizedItemTitle = NormalizeTitle(itemTitle);
|
||||
var normalizedItemFullTitle = NormalizeTitle(itemFullTitle);
|
||||
|
||||
Serilog.Log.Logger.Information("Audiobookshelf duplicate check: comparing search='{SearchTitle}'/'{BaseTitle}' against ABS '{ItemTitle}'/'{ItemFullTitle}' (normalized: '{NormItemTitle}'/'{NormItemFull}')", normalizedTitle, baseTitle, itemTitle, itemFullTitle, normalizedItemTitle, normalizedItemFullTitle);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(normalizedItemTitle))
|
||||
{
|
||||
// 1) Exact match against normalized full title
|
||||
if (string.Equals(normalizedItemTitle, normalizedTitle, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// 2) Exact match against base title (without subtitle)
|
||||
if (string.Equals(normalizedItemTitle, baseTitle, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// 3) Match against combined title:subtitle
|
||||
if (!string.IsNullOrWhiteSpace(normalizedItemFullTitle)
|
||||
&& string.Equals(normalizedItemFullTitle, normalizedTitle, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// 4) Substring fallback: ABS title contains our base title (handles "The Hobbit" vs "The Hobbit: 75th Anniversary Edition")
|
||||
if (!string.IsNullOrWhiteSpace(baseTitle)
|
||||
&& baseTitle.Length > 5
|
||||
&& normalizedItemTitle.Contains(baseTitle, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: use results.Count < limit as primary end-of-pagination indicator.
|
||||
// Do NOT fall back to results.Count for total — that caused breaking after page 1
|
||||
// when the first page had exactly 'limit' items.
|
||||
if (results.Count < limit)
|
||||
break;
|
||||
|
||||
// Secondary: if API provides total, use it to avoid a blank-page round-trip
|
||||
if (obj["total"]?.Value<int>() is int total && (page + 1) * limit >= total)
|
||||
break;
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Audiobookshelf duplicate check: '{SearchTitle}' not found in library {LibraryId}", normalizedTitle, libraryId);
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<bool> UploadBookAsync(
|
||||
private static string NormalizeTitle(string? title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
return title ?? "";
|
||||
|
||||
var normalized = title
|
||||
.Replace("\u00A0", " ")
|
||||
.Replace("(Unabridged)", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("(Abridged)", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Trim();
|
||||
|
||||
// Collapse multiple spaces to single space
|
||||
while (normalized.Contains(" ", StringComparison.Ordinal))
|
||||
normalized = normalized.Replace(" ", " ", StringComparison.Ordinal);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string GetBaseTitle(string fullTitle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fullTitle))
|
||||
return fullTitle;
|
||||
|
||||
var colonIndex = fullTitle.IndexOf(": ", StringComparison.Ordinal);
|
||||
return colonIndex > 0 ? fullTitle[..colonIndex].Trim() : fullTitle;
|
||||
}
|
||||
|
||||
public static async Task<UploadResult> UploadBookAsync(
|
||||
string serverUrl,
|
||||
string apiToken,
|
||||
string libraryId,
|
||||
@@ -95,6 +183,23 @@ public static class AudiobookshelfApiService
|
||||
string? series,
|
||||
IEnumerable<string> filePaths)
|
||||
{
|
||||
apiToken = AudiobookshelfTokenStorage.DecryptToken(apiToken) ?? "";
|
||||
|
||||
// Pre-check for existing item
|
||||
try
|
||||
{
|
||||
if (await BookExistsAsync(serverUrl, apiToken, libraryId, title))
|
||||
{
|
||||
Serilog.Log.Logger.Information("Skipping Audiobookshelf upload: book '{Title}' already exists in library {LibraryId}", title, libraryId);
|
||||
return UploadResult.AlreadyExists;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Pre-check for existing book on Audiobookshelf failed; aborting upload");
|
||||
return UploadResult.Failed;
|
||||
}
|
||||
|
||||
using var client = CreateClient(serverUrl);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
|
||||
client.Timeout = TimeSpan.FromMinutes(30);
|
||||
@@ -111,21 +216,49 @@ public static class AudiobookshelfApiService
|
||||
form.Add(new StringContent(folderId), "folder");
|
||||
|
||||
int fileIndex = 0;
|
||||
foreach (var path in filePaths.Where(File.Exists))
|
||||
var streams = new List<Stream>();
|
||||
try
|
||||
{
|
||||
var fileContent = new StreamContent(File.OpenRead(path));
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
form.Add(fileContent, fileIndex.ToString(), Path.GetFileName(path));
|
||||
fileIndex++;
|
||||
foreach (var path in filePaths.Where(File.Exists))
|
||||
{
|
||||
var stream = File.OpenRead(path);
|
||||
streams.Add(stream);
|
||||
var fileContent = new StreamContent(stream);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
form.Add(fileContent, fileIndex.ToString(), Path.GetFileName(path));
|
||||
fileIndex++;
|
||||
}
|
||||
|
||||
if (fileIndex == 0)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("No audio files found to upload to Audiobookshelf for '{Title}'", title);
|
||||
return UploadResult.Failed;
|
||||
}
|
||||
|
||||
var response = await client.PostAsync("api/upload", form);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return UploadResult.Success;
|
||||
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
Serilog.Log.Logger.Error("Audiobookshelf upload failed for '{Title}' with status {(int)response.StatusCode} ({StatusCode}). Response body: {ResponseBody}",
|
||||
title, (int)response.StatusCode, response.StatusCode, responseBody);
|
||||
|
||||
// Treat "already exists" 500 errors as skip/success
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError
|
||||
&& responseBody.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return UploadResult.AlreadyExists;
|
||||
}
|
||||
|
||||
return UploadResult.Failed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
try { stream.Dispose(); } catch { /* ignored */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (fileIndex == 0)
|
||||
return false;
|
||||
|
||||
var response = await client.PostAsync("api/upload", form);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
private static string JsonEscape(string s)
|
||||
=> s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
|
||||
}
|
||||
|
||||
56
Source/ApplicationServices/AudiobookshelfTokenStorage.cs
Normal file
56
Source/ApplicationServices/AudiobookshelfTokenStorage.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using AudibleApi.Authorization;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace ApplicationServices;
|
||||
|
||||
public static class AudiobookshelfTokenStorage
|
||||
{
|
||||
private const string EncryptedPrefix = "enc:";
|
||||
private const string AssociatedData = "audiobookshelf-api-token";
|
||||
|
||||
public static string? EncryptToken(string? plaintextToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plaintextToken))
|
||||
return plaintextToken;
|
||||
|
||||
if (Configuration.Instance.TokenStorageMethod != TokenStorageMethod.Encrypted)
|
||||
return plaintextToken;
|
||||
|
||||
var protector = IdentityTokenStorage.Protector;
|
||||
if (protector is null)
|
||||
return plaintextToken;
|
||||
|
||||
try
|
||||
{
|
||||
var encrypted = protector.Protect(plaintextToken, AssociatedData);
|
||||
return EncryptedPrefix + encrypted;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return plaintextToken;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? DecryptToken(string? storedToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(storedToken))
|
||||
return storedToken;
|
||||
|
||||
if (!storedToken.StartsWith(EncryptedPrefix))
|
||||
return storedToken;
|
||||
|
||||
var payload = storedToken[EncryptedPrefix.Length..];
|
||||
var protector = IdentityTokenStorage.Protector;
|
||||
if (protector is null)
|
||||
return storedToken;
|
||||
|
||||
try
|
||||
{
|
||||
return protector.Unprotect(payload, AssociatedData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return storedToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class UploadToAudiobookshelf : Processable, IProcessable<UploadToAudioboo
|
||||
if (files.Count == 0)
|
||||
{
|
||||
OnStatusUpdate("No audio files found to upload");
|
||||
return new StatusHandler { "No audio files found to upload" };
|
||||
return new StatusHandler();
|
||||
}
|
||||
|
||||
OnStatusUpdate($"Uploading {files.Count} file(s) to Audiobookshelf...");
|
||||
@@ -47,7 +47,7 @@ public class UploadToAudiobookshelf : Processable, IProcessable<UploadToAudioboo
|
||||
var author = libraryBook.Book.AuthorNames;
|
||||
var series = libraryBook.Book.SeriesNames();
|
||||
|
||||
var success = await AudiobookshelfApiService.UploadBookAsync(
|
||||
var result = await AudiobookshelfApiService.UploadBookAsync(
|
||||
Configuration.AudiobookshelfServerUrl!,
|
||||
Configuration.AudiobookshelfApiToken!,
|
||||
Configuration.AudiobookshelfLibraryId!,
|
||||
@@ -57,21 +57,31 @@ public class UploadToAudiobookshelf : Processable, IProcessable<UploadToAudioboo
|
||||
series,
|
||||
files);
|
||||
|
||||
if (success)
|
||||
if (result == AudiobookshelfApiService.UploadResult.Success)
|
||||
{
|
||||
OnStatusUpdate("Upload to Audiobookshelf completed successfully");
|
||||
return new StatusHandler();
|
||||
}
|
||||
else if (result == AudiobookshelfApiService.UploadResult.AlreadyExists)
|
||||
{
|
||||
OnStatusUpdate("Book already exists on Audiobookshelf; skipping upload");
|
||||
Serilog.Log.Logger.Information("Book already exists on Audiobookshelf, skipping upload: {Book}", libraryBook.LogFriendly());
|
||||
return new StatusHandler();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnStatusUpdate("Upload to Audiobookshelf failed");
|
||||
return new StatusHandler { "Upload to Audiobookshelf failed" };
|
||||
OnStatusUpdate("Upload to Audiobookshelf failed; book remains liberated on disk");
|
||||
Serilog.Log.Logger.Error("Audiobookshelf upload failed for {Book}, but continuing as soft-failure", libraryBook.LogFriendly());
|
||||
// Soft-fail: log the error but do not mark the book as failed
|
||||
return new StatusHandler();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error uploading {Book} to Audiobookshelf", libraryBook.LogFriendly());
|
||||
return new StatusHandler { $"Audiobookshelf upload error: {ex.Message}" };
|
||||
Serilog.Log.Logger.Error(ex, "Error uploading {Book} to Audiobookshelf; continuing as soft-failure", libraryBook.LogFriendly());
|
||||
OnStatusUpdate($"Audiobookshelf upload error: {ex.Message}");
|
||||
// Soft-fail: log the error but do not mark the book as failed
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -93,19 +103,20 @@ public class UploadToAudiobookshelf : Processable, IProcessable<UploadToAudioboo
|
||||
|
||||
files.AddRange(audioFiles);
|
||||
|
||||
// Also look for cover art in the same directory as the first audio file
|
||||
// Use Libation's known cover art output path (same logic as DownloadDecryptBook.DownloadCoverArt)
|
||||
if (audioFiles.FirstOrDefault() is { } firstAudioFile)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(firstAudioFile);
|
||||
if (dir is not null)
|
||||
{
|
||||
var coverFile = Directory.EnumerateFiles(dir, "*.jpg", SearchOption.TopDirectoryOnly)
|
||||
.Concat(Directory.EnumerateFiles(dir, "*.jpeg", SearchOption.TopDirectoryOnly))
|
||||
.Concat(Directory.EnumerateFiles(dir, "*.png", SearchOption.TopDirectoryOnly))
|
||||
.FirstOrDefault();
|
||||
var coverPath = AudibleFileStorage.Audio.GetCustomDirFilename(
|
||||
libraryBook,
|
||||
dir,
|
||||
".jpg",
|
||||
returnFirstExisting: false);
|
||||
|
||||
if (coverFile is not null)
|
||||
files.Add(coverFile);
|
||||
if (File.Exists(coverPath))
|
||||
files.Add(coverPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,71 +9,86 @@
|
||||
|
||||
<StackPanel Margin="6" Spacing="0">
|
||||
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay}"
|
||||
Content="Automatically upload downloaded books to Audiobookshelf"
|
||||
Content="{Binding EnabledText}"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
<!-- Server URL -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="Server URL"
|
||||
Text="{Binding ServerUrlText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,6" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1"
|
||||
Text="{Binding ServerUrl, Mode=TwoWay}"
|
||||
PlaceholderText="https://abs.example.com"
|
||||
Margin="0,0,0,6" />
|
||||
IsEnabled="{Binding Enabled}"
|
||||
Margin="0,0,10,6" />
|
||||
|
||||
<!-- API Token -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
Text="API Token"
|
||||
Text="{Binding ApiTokenText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,10" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1"
|
||||
Text="{Binding ApiToken, Mode=TwoWay}"
|
||||
PasswordChar="*"
|
||||
PlaceholderText="Your ABS API token"
|
||||
Margin="0,0,0,10" />
|
||||
IsEnabled="{Binding Enabled}"
|
||||
Margin="0,0,10,10" />
|
||||
|
||||
<!-- Connect Button -->
|
||||
<Button Grid.Row="2" Grid.Column="1"
|
||||
Content="Connect / Refresh"
|
||||
<Button Grid.Row="0" Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
Content="{Binding ConnectButtonText}"
|
||||
Command="{Binding ConnectCommand}"
|
||||
IsEnabled="{Binding !IsConnecting}"
|
||||
IsEnabled="{Binding Enabled}"
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalContentAlignment="Center"
|
||||
MinWidth="150"
|
||||
MinHeight="34"
|
||||
VerticalAlignment="Stretch"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<!-- Plaintext warning -->
|
||||
<TextBlock Grid.Row="2" Grid.Column="1"
|
||||
Text="{Binding PlaintextWarningText}"
|
||||
Foreground="DarkOrange"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding PlaintextWarningVisible}"
|
||||
Margin="0,0,0,10" />
|
||||
|
||||
<!-- Status -->
|
||||
<ProgressBar Grid.Row="3" Grid.Column="1"
|
||||
<ProgressBar Grid.Row="4" Grid.Column="1"
|
||||
IsIndeterminate="True"
|
||||
IsVisible="{Binding IsConnecting}"
|
||||
Margin="0,0,0,6" />
|
||||
<TextBlock Grid.Row="4" Grid.Column="1"
|
||||
<TextBlock Grid.Row="5" Grid.Column="1"
|
||||
Text="{Binding StatusText}"
|
||||
TextWrapping="Wrap"
|
||||
IsVisible="{Binding !IsConnecting}"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<!-- Library -->
|
||||
<TextBlock Grid.Row="5" Grid.Column="0"
|
||||
Text="Library"
|
||||
<TextBlock Grid.Row="6" Grid.Column="0"
|
||||
Text="{Binding LibraryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,7" />
|
||||
<ComboBox Grid.Row="5" Grid.Column="1"
|
||||
<ComboBox Grid.Row="6" Grid.Column="1"
|
||||
ItemsSource="{Binding LibraryNames}"
|
||||
SelectedIndex="{Binding SelectedLibraryIndex, Mode=TwoWay}"
|
||||
IsEnabled="{Binding Enabled}"
|
||||
Margin="0,0,0,7" />
|
||||
|
||||
<!-- Folder -->
|
||||
<TextBlock Grid.Row="6" Grid.Column="0"
|
||||
Text="Folder"
|
||||
<TextBlock Grid.Row="7" Grid.Column="0"
|
||||
Text="{Binding FolderText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0" />
|
||||
<ComboBox Grid.Row="6" Grid.Column="1"
|
||||
<ComboBox Grid.Row="7" Grid.Column="1"
|
||||
ItemsSource="{Binding FolderNames}"
|
||||
SelectedIndex="{Binding SelectedFolderIndex, Mode=TwoWay}" />
|
||||
SelectedIndex="{Binding SelectedFolderIndex, Mode=TwoWay}"
|
||||
IsEnabled="{Binding Enabled}"
|
||||
Margin="0,0,0,0" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using ApplicationServices;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
@@ -17,6 +20,7 @@ public class AudiobookshelfSettingsVM : ViewModelBase
|
||||
private string apiToken = "";
|
||||
private string statusText = "";
|
||||
private bool statusIsError;
|
||||
private bool plaintextWarningVisible;
|
||||
private List<AudiobookshelfApiService.Library> libraries = [];
|
||||
private int selectedLibraryIndex = -1;
|
||||
private int selectedFolderIndex = -1;
|
||||
@@ -29,7 +33,10 @@ public class AudiobookshelfSettingsVM : ViewModelBase
|
||||
this.config = config;
|
||||
enabled = config.AudiobookshelfEnabled;
|
||||
serverUrl = config.AudiobookshelfServerUrl ?? "";
|
||||
apiToken = config.AudiobookshelfApiToken ?? "";
|
||||
apiToken = AudiobookshelfTokenStorage.DecryptToken(config.AudiobookshelfApiToken) ?? "";
|
||||
|
||||
var osSecretStoreAvailable = IdentityTokenStorageWiring.IsOsSecretStoreAvailable(out _);
|
||||
PlaintextWarningVisible = osSecretStoreAvailable && config.TokenStorageMethod == TokenStorageMethod.Plaintext;
|
||||
|
||||
ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync);
|
||||
_ = RestoreLibrariesAsync();
|
||||
@@ -65,6 +72,12 @@ public class AudiobookshelfSettingsVM : ViewModelBase
|
||||
set => this.RaiseAndSetIfChanged(ref statusIsError, value);
|
||||
}
|
||||
|
||||
public bool PlaintextWarningVisible
|
||||
{
|
||||
get => plaintextWarningVisible;
|
||||
private set => this.RaiseAndSetIfChanged(ref plaintextWarningVisible, value);
|
||||
}
|
||||
|
||||
public List<AudiobookshelfApiService.Library> Libraries
|
||||
{
|
||||
get => libraries;
|
||||
@@ -113,6 +126,15 @@ public class AudiobookshelfSettingsVM : ViewModelBase
|
||||
|
||||
public ICommand ConnectCommand { get; }
|
||||
|
||||
// Labels from Configuration descriptions
|
||||
public string EnabledText { get; } = Configuration.GetDescription(nameof(Configuration.AudiobookshelfEnabled));
|
||||
public string ServerUrlText { get; } = Configuration.GetDescription(nameof(Configuration.AudiobookshelfServerUrl));
|
||||
public string ApiTokenText { get; } = Configuration.GetDescription(nameof(Configuration.AudiobookshelfApiToken));
|
||||
public string LibraryText { get; } = Configuration.GetDescription(nameof(Configuration.AudiobookshelfLibraryId));
|
||||
public string FolderText { get; } = Configuration.GetDescription(nameof(Configuration.AudiobookshelfFolderId));
|
||||
public string PlaintextWarningText { get; } = "Warning: The API token is stored as plaintext in Settings.json.";
|
||||
public string ConnectButtonText { get; } = "Connect / Refresh";
|
||||
|
||||
private void UpdateLibraryNames()
|
||||
{
|
||||
LibraryNames.Clear();
|
||||
@@ -154,7 +176,7 @@ public class AudiobookshelfSettingsVM : ViewModelBase
|
||||
var libs = await AudiobookshelfApiService.GetLibrariesAsync(ServerUrl.Trim(), ApiToken.Trim());
|
||||
if (libs.Count == 0)
|
||||
{
|
||||
StatusText = "Could not connect or no libraries found. Check your settings.";
|
||||
StatusText = "No book libraries found. Check your settings.";
|
||||
StatusIsError = true;
|
||||
return;
|
||||
}
|
||||
@@ -206,7 +228,7 @@ public class AudiobookshelfSettingsVM : ViewModelBase
|
||||
{
|
||||
config.AudiobookshelfEnabled = Enabled;
|
||||
config.AudiobookshelfServerUrl = ServerUrl.Trim();
|
||||
config.AudiobookshelfApiToken = ApiToken.Trim();
|
||||
config.AudiobookshelfApiToken = AudiobookshelfTokenStorage.EncryptToken(ApiToken.Trim());
|
||||
|
||||
if (SelectedLibraryIndex >= 0 && Libraries.Count > SelectedLibraryIndex)
|
||||
config.AudiobookshelfLibraryId = Libraries[SelectedLibraryIndex].Id;
|
||||
|
||||
@@ -144,12 +144,14 @@ public class LiberateOptions : ProcessableOptionsBase
|
||||
private static Processable CreateBackupBook(DownloadOptions.LicenseInfo? licenseInfo)
|
||||
{
|
||||
var downloadPdf = CreateProcessable<DownloadPdf>();
|
||||
var uploadToAudiobookshelf = CreateProcessable<UploadToAudiobookshelf>();
|
||||
|
||||
//Chain pdf download on DownloadDecryptBook.Completed
|
||||
// Chain pdf download and audiobookshelf upload on DownloadDecryptBook.Completed
|
||||
void onDownloadDecryptBookCompleted(object? sender, LibraryBook e)
|
||||
{
|
||||
// this is fast anyway. run as sync for easy exception catching
|
||||
downloadPdf.TryProcessAsync(e).GetAwaiter().GetResult();
|
||||
uploadToAudiobookshelf.TryProcessAsync(e).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook>(onDownloadDecryptBookCompleted);
|
||||
|
||||
@@ -319,19 +319,19 @@ public partial class Configuration
|
||||
public bool RequestSpatial { get => false; set { } } // { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
#region Audiobookshelf
|
||||
[Description("Automatically upload downloaded books to Audiobookshelf")]
|
||||
[Description("Automatically upload downloaded books")]
|
||||
public bool AudiobookshelfEnabled { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Audiobookshelf server URL")]
|
||||
[Description("Server URL")]
|
||||
public string? AudiobookshelfServerUrl { get => GetString(); set => SetString(value); }
|
||||
|
||||
[Description("Audiobookshelf API token")]
|
||||
[Description("API Token")]
|
||||
public string? AudiobookshelfApiToken { get => GetString(); set => SetString(value); }
|
||||
|
||||
[Description("Audiobookshelf library ID")]
|
||||
[Description("Library")]
|
||||
public string? AudiobookshelfLibraryId { get => GetString(); set => SetString(value); }
|
||||
|
||||
[Description("Audiobookshelf folder ID")]
|
||||
[Description("Folder")]
|
||||
public string? AudiobookshelfFolderId { get => GetString(); set => SetString(value); }
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
processable.Begin += Processable_Begin;
|
||||
processable.Completed += Processable_Completed;
|
||||
processable.StatusUpdate += Processable_StatusUpdate;
|
||||
processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
|
||||
processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
|
||||
|
||||
@@ -303,6 +304,7 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
processable.Begin -= Processable_Begin;
|
||||
processable.Completed -= Processable_Completed;
|
||||
processable.StatusUpdate -= Processable_StatusUpdate;
|
||||
processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
|
||||
processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
|
||||
|
||||
@@ -372,6 +374,9 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
Narrator = libraryBook.Book.NarratorNames;
|
||||
}
|
||||
|
||||
private void Processable_StatusUpdate(object? sender, string statusUpdate)
|
||||
=> LogInfo(statusUpdate);
|
||||
|
||||
private void Processable_Completed(object? sender, LibraryBook libraryBook)
|
||||
{
|
||||
if (sender is Processable processable)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using ApplicationServices;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -10,38 +12,26 @@ namespace LibationWinForms.Dialogs;
|
||||
|
||||
public partial class SettingsDialog
|
||||
{
|
||||
private TabPage? tab5Audiobookshelf;
|
||||
private CheckBox? absEnabledCb;
|
||||
private TextBox? absUrlTb;
|
||||
private TextBox? absTokenTb;
|
||||
private Button? absConnectBtn;
|
||||
private ComboBox? absLibraryCb;
|
||||
private ComboBox? absFolderCb;
|
||||
private Label? absUrlLbl;
|
||||
private Label? absTokenLbl;
|
||||
private Label? absLibraryLbl;
|
||||
private Label? absFolderLbl;
|
||||
private Label? absStatusLbl;
|
||||
|
||||
private List<AudiobookshelfApiService.Library> _absLibraries = [];
|
||||
|
||||
private void Load_Audiobookshelf(Configuration config)
|
||||
{
|
||||
CreateAudiobookshelfTab();
|
||||
|
||||
absEnabledCb!.Text = desc(nameof(config.AudiobookshelfEnabled));
|
||||
absUrlLbl!.Text = "Server URL";
|
||||
absTokenLbl!.Text = "API Token";
|
||||
absLibraryLbl!.Text = "Library";
|
||||
absFolderLbl!.Text = "Folder";
|
||||
absConnectBtn!.Text = "Connect / Refresh";
|
||||
absStatusLbl!.Text = "";
|
||||
absEnabledCb.Text = desc(nameof(config.AudiobookshelfEnabled));
|
||||
absUrlLbl.Text = desc(nameof(config.AudiobookshelfServerUrl));
|
||||
absTokenLbl.Text = desc(nameof(config.AudiobookshelfApiToken));
|
||||
absLibraryLbl.Text = desc(nameof(config.AudiobookshelfLibraryId));
|
||||
absFolderLbl.Text = desc(nameof(config.AudiobookshelfFolderId));
|
||||
absConnectBtn.Text = "Connect / Refresh";
|
||||
absStatusLbl.Text = "";
|
||||
|
||||
absEnabledCb.Checked = config.AudiobookshelfEnabled;
|
||||
absUrlTb!.Text = config.AudiobookshelfServerUrl ?? "";
|
||||
absTokenTb!.Text = config.AudiobookshelfApiToken ?? "";
|
||||
absUrlTb.Text = config.AudiobookshelfServerUrl ?? "";
|
||||
absTokenTb.Text = AudiobookshelfTokenStorage.DecryptToken(config.AudiobookshelfApiToken) ?? "";
|
||||
absTokenTb.PasswordChar = '*';
|
||||
|
||||
var osSecretStoreAvailable = IdentityTokenStorageWiring.IsOsSecretStoreAvailable(out _);
|
||||
absPlaintextWarningLbl.Visible = osSecretStoreAvailable && config.TokenStorageMethod == TokenStorageMethod.Plaintext;
|
||||
|
||||
ToggleAudiobookshelfControls(absEnabledCb.Checked);
|
||||
|
||||
// Try to restore saved selections if we have them
|
||||
@@ -49,163 +39,28 @@ public partial class SettingsDialog
|
||||
_ = RestoreLibraryAndFolderAsync(config);
|
||||
}
|
||||
|
||||
private void CreateAudiobookshelfTab()
|
||||
{
|
||||
tab5Audiobookshelf = new TabPage
|
||||
{
|
||||
AutoScroll = true,
|
||||
BackColor = System.Drawing.SystemColors.Window,
|
||||
Location = new System.Drawing.Point(4, 24),
|
||||
Name = "tab5Audiobookshelf",
|
||||
Padding = new Padding(3),
|
||||
Size = new System.Drawing.Size(856, 457),
|
||||
TabIndex = 4,
|
||||
Text = "Audiobookshelf"
|
||||
};
|
||||
|
||||
absEnabledCb = new CheckBox
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new System.Drawing.Point(6, 6),
|
||||
Name = "absEnabledCb",
|
||||
Size = new System.Drawing.Size(280, 19),
|
||||
TabIndex = 0,
|
||||
Text = "[AudiobookshelfEnabled desc]",
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
absEnabledCb.CheckedChanged += absEnabledCb_CheckedChanged;
|
||||
|
||||
absUrlLbl = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new System.Drawing.Point(6, 35),
|
||||
Size = new System.Drawing.Size(70, 15),
|
||||
TabIndex = 1,
|
||||
Text = "Server URL"
|
||||
};
|
||||
|
||||
absUrlTb = new TextBox
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
|
||||
Location = new System.Drawing.Point(90, 32),
|
||||
Size = new System.Drawing.Size(550, 23),
|
||||
TabIndex = 2
|
||||
};
|
||||
|
||||
absTokenLbl = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new System.Drawing.Point(6, 64),
|
||||
Size = new System.Drawing.Size(65, 15),
|
||||
TabIndex = 3,
|
||||
Text = "API Token"
|
||||
};
|
||||
|
||||
absTokenTb = new TextBox
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
|
||||
Location = new System.Drawing.Point(90, 61),
|
||||
Size = new System.Drawing.Size(550, 23),
|
||||
TabIndex = 4
|
||||
};
|
||||
|
||||
absConnectBtn = new Button
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Right,
|
||||
Location = new System.Drawing.Point(646, 32),
|
||||
Size = new System.Drawing.Size(130, 52),
|
||||
TabIndex = 5,
|
||||
Text = "Connect / Refresh",
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
absConnectBtn.Click += absConnectBtn_Click;
|
||||
|
||||
absStatusLbl = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new System.Drawing.Point(6, 94),
|
||||
Size = new System.Drawing.Size(0, 15),
|
||||
TabIndex = 6,
|
||||
Text = ""
|
||||
};
|
||||
|
||||
absLibraryLbl = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new System.Drawing.Point(6, 120),
|
||||
Size = new System.Drawing.Size(50, 15),
|
||||
TabIndex = 7,
|
||||
Text = "Library"
|
||||
};
|
||||
|
||||
absLibraryCb = new ComboBox
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
FormattingEnabled = true,
|
||||
Location = new System.Drawing.Point(90, 117),
|
||||
Size = new System.Drawing.Size(686, 23),
|
||||
TabIndex = 8
|
||||
};
|
||||
absLibraryCb.SelectedIndexChanged += absLibraryCb_SelectedIndexChanged;
|
||||
|
||||
absFolderLbl = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Location = new System.Drawing.Point(6, 150),
|
||||
Size = new System.Drawing.Size(45, 15),
|
||||
TabIndex = 9,
|
||||
Text = "Folder"
|
||||
};
|
||||
|
||||
absFolderCb = new ComboBox
|
||||
{
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
|
||||
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||
FormattingEnabled = true,
|
||||
Location = new System.Drawing.Point(90, 147),
|
||||
Size = new System.Drawing.Size(686, 23),
|
||||
TabIndex = 10
|
||||
};
|
||||
|
||||
tab5Audiobookshelf.Controls.Add(absEnabledCb);
|
||||
tab5Audiobookshelf.Controls.Add(absUrlLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absUrlTb);
|
||||
tab5Audiobookshelf.Controls.Add(absTokenLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absTokenTb);
|
||||
tab5Audiobookshelf.Controls.Add(absConnectBtn);
|
||||
tab5Audiobookshelf.Controls.Add(absStatusLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absLibraryLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absLibraryCb);
|
||||
tab5Audiobookshelf.Controls.Add(absFolderLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absFolderCb);
|
||||
|
||||
tabControl.Controls.Add(tab5Audiobookshelf);
|
||||
}
|
||||
|
||||
private void ToggleAudiobookshelfControls(bool enabled)
|
||||
{
|
||||
if (absUrlTb is null) return;
|
||||
absUrlTb.Enabled = enabled;
|
||||
absTokenTb!.Enabled = enabled;
|
||||
absConnectBtn!.Enabled = enabled;
|
||||
absLibraryCb!.Enabled = enabled && _absLibraries.Count > 0;
|
||||
absFolderCb!.Enabled = enabled && absLibraryCb!.SelectedIndex >= 0;
|
||||
absTokenTb.Enabled = enabled;
|
||||
absConnectBtn.Enabled = enabled;
|
||||
absLibraryCb.Enabled = enabled && _absLibraries.Count > 0;
|
||||
absFolderCb.Enabled = enabled && absLibraryCb.SelectedIndex >= 0;
|
||||
}
|
||||
|
||||
private void absEnabledCb_CheckedChanged(object? sender, EventArgs e)
|
||||
=> ToggleAudiobookshelfControls(absEnabledCb!.Checked);
|
||||
=> ToggleAudiobookshelfControls(absEnabledCb.Checked);
|
||||
|
||||
private async void absConnectBtn_Click(object? sender, EventArgs e)
|
||||
{
|
||||
absConnectBtn!.Enabled = false;
|
||||
absStatusLbl!.Text = "Connecting...";
|
||||
absConnectBtn.Enabled = false;
|
||||
absStatusLbl.Text = "Connecting...";
|
||||
absStatusLbl.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
|
||||
try
|
||||
{
|
||||
var url = absUrlTb!.Text.Trim();
|
||||
var token = absTokenTb!.Text.Trim();
|
||||
var url = absUrlTb.Text.Trim();
|
||||
var token = absTokenTb.Text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
@@ -218,20 +73,20 @@ public partial class SettingsDialog
|
||||
|
||||
if (libraries.Count == 0)
|
||||
{
|
||||
absStatusLbl.Text = "Could not connect or no libraries found. Check your settings.";
|
||||
absStatusLbl.Text = "No book libraries found. Check your settings.";
|
||||
absStatusLbl.ForeColor = System.Drawing.Color.DarkRed;
|
||||
return;
|
||||
}
|
||||
|
||||
_absLibraries = libraries;
|
||||
absLibraryCb!.Items.Clear();
|
||||
absLibraryCb.Items.Clear();
|
||||
absLibraryCb.Items.AddRange(libraries.Select(l => $"{l.Name} ({l.Id})").ToArray());
|
||||
absFolderCb!.Items.Clear();
|
||||
absFolderCb.Items.Clear();
|
||||
absFolderCb.SelectedIndex = -1;
|
||||
|
||||
absStatusLbl.Text = $"Connected. Found {libraries.Count} library(ies).";
|
||||
absStatusLbl.ForeColor = System.Drawing.Color.DarkGreen;
|
||||
ToggleAudiobookshelfControls(absEnabledCb!.Checked);
|
||||
ToggleAudiobookshelfControls(absEnabledCb.Checked);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -246,10 +101,10 @@ public partial class SettingsDialog
|
||||
|
||||
private void absLibraryCb_SelectedIndexChanged(object? sender, EventArgs e)
|
||||
{
|
||||
absFolderCb!.Items.Clear();
|
||||
absFolderCb.Items.Clear();
|
||||
absFolderCb.SelectedIndex = -1;
|
||||
|
||||
if (absLibraryCb!.SelectedIndex < 0 || _absLibraries.Count == 0)
|
||||
if (absLibraryCb.SelectedIndex < 0 || _absLibraries.Count == 0)
|
||||
return;
|
||||
|
||||
var library = _absLibraries[absLibraryCb.SelectedIndex];
|
||||
@@ -272,7 +127,7 @@ public partial class SettingsDialog
|
||||
var libraries = await AudiobookshelfApiService.GetLibrariesAsync(config.AudiobookshelfServerUrl, config.AudiobookshelfApiToken);
|
||||
_absLibraries = libraries;
|
||||
|
||||
absLibraryCb!.Items.Clear();
|
||||
absLibraryCb.Items.Clear();
|
||||
absLibraryCb.Items.AddRange(libraries.Select(l => $"{l.Name} ({l.Id})").ToArray());
|
||||
|
||||
var savedLibIndex = libraries.FindIndex(l => l.Id == config.AudiobookshelfLibraryId);
|
||||
@@ -282,28 +137,28 @@ public partial class SettingsDialog
|
||||
var library = libraries[savedLibIndex];
|
||||
var savedFolderIndex = library.Folders.FindIndex(f => f.Id == config.AudiobookshelfFolderId);
|
||||
if (savedFolderIndex >= 0)
|
||||
absFolderCb!.SelectedIndex = savedFolderIndex;
|
||||
absFolderCb.SelectedIndex = savedFolderIndex;
|
||||
}
|
||||
ToggleAudiobookshelfControls(absEnabledCb!.Checked);
|
||||
ToggleAudiobookshelfControls(absEnabledCb.Checked);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Silently fail; user can manually reconnect
|
||||
Serilog.Log.Logger.Warning(ex, "Failed to restore Audiobookshelf library and folder selections");
|
||||
}
|
||||
}
|
||||
|
||||
private void Save_Audiobookshelf(Configuration config)
|
||||
{
|
||||
config.AudiobookshelfEnabled = absEnabledCb!.Checked;
|
||||
config.AudiobookshelfServerUrl = absUrlTb!.Text.Trim();
|
||||
config.AudiobookshelfApiToken = absTokenTb!.Text.Trim();
|
||||
config.AudiobookshelfEnabled = absEnabledCb.Checked;
|
||||
config.AudiobookshelfServerUrl = absUrlTb.Text.Trim();
|
||||
config.AudiobookshelfApiToken = AudiobookshelfTokenStorage.EncryptToken(absTokenTb.Text.Trim());
|
||||
|
||||
if (absLibraryCb!.SelectedIndex >= 0 && _absLibraries.Count > absLibraryCb.SelectedIndex)
|
||||
if (absLibraryCb.SelectedIndex >= 0 && _absLibraries.Count > absLibraryCb.SelectedIndex)
|
||||
config.AudiobookshelfLibraryId = _absLibraries[absLibraryCb.SelectedIndex].Id;
|
||||
else
|
||||
config.AudiobookshelfLibraryId = null;
|
||||
|
||||
if (absFolderCb!.SelectedIndex >= 0 && absLibraryCb.SelectedIndex >= 0)
|
||||
if (absFolderCb.SelectedIndex >= 0 && absLibraryCb.SelectedIndex >= 0)
|
||||
{
|
||||
var library = _absLibraries[absLibraryCb.SelectedIndex];
|
||||
if (library.Folders.Count > absFolderCb.SelectedIndex)
|
||||
|
||||
@@ -48,6 +48,19 @@
|
||||
loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
tabControl = new System.Windows.Forms.TabControl();
|
||||
tab5Audiobookshelf = new System.Windows.Forms.TabPage();
|
||||
absEnabledCb = new System.Windows.Forms.CheckBox();
|
||||
absPlaintextWarningLbl = new System.Windows.Forms.Label();
|
||||
absUrlLbl = new System.Windows.Forms.Label();
|
||||
absUrlTb = new System.Windows.Forms.TextBox();
|
||||
absTokenLbl = new System.Windows.Forms.Label();
|
||||
absTokenTb = new System.Windows.Forms.TextBox();
|
||||
absConnectBtn = new System.Windows.Forms.Button();
|
||||
absStatusLbl = new System.Windows.Forms.Label();
|
||||
absLibraryLbl = new System.Windows.Forms.Label();
|
||||
absLibraryCb = new System.Windows.Forms.ComboBox();
|
||||
absFolderLbl = new System.Windows.Forms.Label();
|
||||
absFolderCb = new System.Windows.Forms.ComboBox();
|
||||
tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
||||
themeLbl = new System.Windows.Forms.Label();
|
||||
themeCb = new System.Windows.Forms.ComboBox();
|
||||
@@ -160,6 +173,7 @@
|
||||
inProgressFilesGb.SuspendLayout();
|
||||
customFileNamingGb.SuspendLayout();
|
||||
tab4AudioFileOptions.SuspendLayout();
|
||||
tab5Audiobookshelf.SuspendLayout();
|
||||
audiobookFixupsGb.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)minFileDurationNud).BeginInit();
|
||||
chapterTitleTemplateGb.SuspendLayout();
|
||||
@@ -388,6 +402,7 @@
|
||||
tabControl.Controls.Add(tab2ImportLibrary);
|
||||
tabControl.Controls.Add(tab3DownloadDecrypt);
|
||||
tabControl.Controls.Add(tab4AudioFileOptions);
|
||||
tabControl.Controls.Add(tab5Audiobookshelf);
|
||||
tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
tabControl.Name = "tabControl";
|
||||
tabControl.SelectedIndex = 0;
|
||||
@@ -924,6 +939,29 @@
|
||||
tab4AudioFileOptions.TabIndex = 3;
|
||||
tab4AudioFileOptions.Text = "Audio File Options";
|
||||
//
|
||||
// tab5Audiobookshelf
|
||||
//
|
||||
tab5Audiobookshelf.AutoScroll = true;
|
||||
tab5Audiobookshelf.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab5Audiobookshelf.Controls.Add(absEnabledCb);
|
||||
tab5Audiobookshelf.Controls.Add(absPlaintextWarningLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absUrlLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absUrlTb);
|
||||
tab5Audiobookshelf.Controls.Add(absTokenLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absTokenTb);
|
||||
tab5Audiobookshelf.Controls.Add(absConnectBtn);
|
||||
tab5Audiobookshelf.Controls.Add(absStatusLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absLibraryLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absLibraryCb);
|
||||
tab5Audiobookshelf.Controls.Add(absFolderLbl);
|
||||
tab5Audiobookshelf.Controls.Add(absFolderCb);
|
||||
tab5Audiobookshelf.Location = new System.Drawing.Point(4, 24);
|
||||
tab5Audiobookshelf.Name = "tab5Audiobookshelf";
|
||||
tab5Audiobookshelf.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab5Audiobookshelf.Size = new System.Drawing.Size(856, 457);
|
||||
tab5Audiobookshelf.TabIndex = 4;
|
||||
tab5Audiobookshelf.Text = "Audiobookshelf";
|
||||
//
|
||||
// request_xHE_AAC_Cbox
|
||||
//
|
||||
request_xHE_AAC_Cbox.CheckAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
@@ -1531,6 +1569,120 @@
|
||||
createCueSheetCbox.UseVisualStyleBackColor = true;
|
||||
createCueSheetCbox.CheckedChanged += allowLibationFixupCbox_CheckedChanged;
|
||||
//
|
||||
// absEnabledCb
|
||||
//
|
||||
absEnabledCb.AutoSize = true;
|
||||
absEnabledCb.Location = new System.Drawing.Point(6, 6);
|
||||
absEnabledCb.Name = "absEnabledCb";
|
||||
absEnabledCb.Size = new System.Drawing.Size(280, 19);
|
||||
absEnabledCb.TabIndex = 0;
|
||||
absEnabledCb.Text = "[AudiobookshelfEnabled desc]";
|
||||
absEnabledCb.UseVisualStyleBackColor = true;
|
||||
absEnabledCb.CheckedChanged += absEnabledCb_CheckedChanged;
|
||||
//
|
||||
// absPlaintextWarningLbl
|
||||
//
|
||||
absPlaintextWarningLbl.AutoSize = true;
|
||||
absPlaintextWarningLbl.ForeColor = System.Drawing.Color.DarkOrange;
|
||||
absPlaintextWarningLbl.Location = new System.Drawing.Point(6, 30);
|
||||
absPlaintextWarningLbl.Name = "absPlaintextWarningLbl";
|
||||
absPlaintextWarningLbl.Size = new System.Drawing.Size(480, 15);
|
||||
absPlaintextWarningLbl.TabIndex = 1;
|
||||
absPlaintextWarningLbl.Text = "Warning: The API token is stored as plaintext in Settings.json.";
|
||||
//
|
||||
// absUrlLbl
|
||||
//
|
||||
absUrlLbl.AutoSize = true;
|
||||
absUrlLbl.Location = new System.Drawing.Point(6, 55);
|
||||
absUrlLbl.Name = "absUrlLbl";
|
||||
absUrlLbl.Size = new System.Drawing.Size(70, 15);
|
||||
absUrlLbl.TabIndex = 2;
|
||||
absUrlLbl.Text = "[AudiobookshelfServerUrl desc]";
|
||||
//
|
||||
// absUrlTb
|
||||
//
|
||||
absUrlTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
absUrlTb.Location = new System.Drawing.Point(90, 52);
|
||||
absUrlTb.Name = "absUrlTb";
|
||||
absUrlTb.Size = new System.Drawing.Size(550, 23);
|
||||
absUrlTb.TabIndex = 3;
|
||||
//
|
||||
// absTokenLbl
|
||||
//
|
||||
absTokenLbl.AutoSize = true;
|
||||
absTokenLbl.Location = new System.Drawing.Point(6, 84);
|
||||
absTokenLbl.Name = "absTokenLbl";
|
||||
absTokenLbl.Size = new System.Drawing.Size(65, 15);
|
||||
absTokenLbl.TabIndex = 4;
|
||||
absTokenLbl.Text = "[AudiobookshelfApiToken desc]";
|
||||
//
|
||||
// absTokenTb
|
||||
//
|
||||
absTokenTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
absTokenTb.Location = new System.Drawing.Point(90, 81);
|
||||
absTokenTb.Name = "absTokenTb";
|
||||
absTokenTb.Size = new System.Drawing.Size(550, 23);
|
||||
absTokenTb.TabIndex = 5;
|
||||
//
|
||||
// absConnectBtn
|
||||
//
|
||||
absConnectBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
absConnectBtn.Location = new System.Drawing.Point(646, 52);
|
||||
absConnectBtn.Name = "absConnectBtn";
|
||||
absConnectBtn.Size = new System.Drawing.Size(130, 52);
|
||||
absConnectBtn.TabIndex = 6;
|
||||
absConnectBtn.Text = "Connect / Refresh";
|
||||
absConnectBtn.UseVisualStyleBackColor = true;
|
||||
absConnectBtn.Click += absConnectBtn_Click;
|
||||
//
|
||||
// absStatusLbl
|
||||
//
|
||||
absStatusLbl.AutoSize = true;
|
||||
absStatusLbl.Location = new System.Drawing.Point(6, 114);
|
||||
absStatusLbl.Name = "absStatusLbl";
|
||||
absStatusLbl.Size = new System.Drawing.Size(0, 15);
|
||||
absStatusLbl.TabIndex = 7;
|
||||
absStatusLbl.Text = "";
|
||||
//
|
||||
// absLibraryLbl
|
||||
//
|
||||
absLibraryLbl.AutoSize = true;
|
||||
absLibraryLbl.Location = new System.Drawing.Point(6, 140);
|
||||
absLibraryLbl.Name = "absLibraryLbl";
|
||||
absLibraryLbl.Size = new System.Drawing.Size(50, 15);
|
||||
absLibraryLbl.TabIndex = 8;
|
||||
absLibraryLbl.Text = "[AudiobookshelfLibraryId desc]";
|
||||
//
|
||||
// absLibraryCb
|
||||
//
|
||||
absLibraryCb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
absLibraryCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
absLibraryCb.FormattingEnabled = true;
|
||||
absLibraryCb.Location = new System.Drawing.Point(90, 137);
|
||||
absLibraryCb.Name = "absLibraryCb";
|
||||
absLibraryCb.Size = new System.Drawing.Size(686, 23);
|
||||
absLibraryCb.TabIndex = 9;
|
||||
absLibraryCb.SelectedIndexChanged += absLibraryCb_SelectedIndexChanged;
|
||||
//
|
||||
// absFolderLbl
|
||||
//
|
||||
absFolderLbl.AutoSize = true;
|
||||
absFolderLbl.Location = new System.Drawing.Point(6, 170);
|
||||
absFolderLbl.Name = "absFolderLbl";
|
||||
absFolderLbl.Size = new System.Drawing.Size(45, 15);
|
||||
absFolderLbl.TabIndex = 10;
|
||||
absFolderLbl.Text = "[AudiobookshelfFolderId desc]";
|
||||
//
|
||||
// absFolderCb
|
||||
//
|
||||
absFolderCb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
absFolderCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
absFolderCb.FormattingEnabled = true;
|
||||
absFolderCb.Location = new System.Drawing.Point(90, 167);
|
||||
absFolderCb.Name = "absFolderCb";
|
||||
absFolderCb.Size = new System.Drawing.Size(686, 23);
|
||||
absFolderCb.TabIndex = 11;
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
AcceptButton = saveBtn;
|
||||
@@ -1586,6 +1738,8 @@
|
||||
((System.ComponentModel.ISupportInitialize)lameVBRQualityTb).EndInit();
|
||||
groupBox2.ResumeLayout(false);
|
||||
groupBox2.PerformLayout();
|
||||
tab5Audiobookshelf.ResumeLayout(false);
|
||||
tab5Audiobookshelf.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
}
|
||||
|
||||
@@ -1709,5 +1863,18 @@
|
||||
private System.Windows.Forms.CheckBox importPlusTitlesCb;
|
||||
private System.Windows.Forms.Label minFileDurationLbl;
|
||||
private System.Windows.Forms.NumericUpDown minFileDurationNud;
|
||||
private System.Windows.Forms.TabPage tab5Audiobookshelf;
|
||||
private System.Windows.Forms.CheckBox absEnabledCb;
|
||||
private System.Windows.Forms.Label absPlaintextWarningLbl;
|
||||
private System.Windows.Forms.Label absUrlLbl;
|
||||
private System.Windows.Forms.TextBox absUrlTb;
|
||||
private System.Windows.Forms.Label absTokenLbl;
|
||||
private System.Windows.Forms.TextBox absTokenTb;
|
||||
private System.Windows.Forms.Button absConnectBtn;
|
||||
private System.Windows.Forms.Label absStatusLbl;
|
||||
private System.Windows.Forms.Label absLibraryLbl;
|
||||
private System.Windows.Forms.ComboBox absLibraryCb;
|
||||
private System.Windows.Forms.Label absFolderLbl;
|
||||
private System.Windows.Forms.ComboBox absFolderCb;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user