Add Audiobookshelf auto-upload integration

- Persist settings: enabled, server URL, API token, library/folder IDs

- Add AudiobookshelfApiService for login, library listing, and multipart upload

- Add UploadToAudiobookshelf post-download processable

- Add settings tab to WinForms and Avalonia with library/folder dropdowns

- Match Avalonia layout to WinForms with aligned columns
This commit is contained in:
John Doe
2026-07-26 09:58:12 +02:00
parent 3580f6d53a
commit ad61618cc9
12 changed files with 921 additions and 1 deletions

View File

@@ -0,0 +1,131 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace ApplicationServices;
public static class AudiobookshelfApiService
{
public record Library(string Id, string Name, List<Folder> Folders);
public record Folder(string Id, string FullPath);
public record LoginResponse(string Token);
private static HttpClient CreateClient(string serverUrl)
{
var client = new HttpClient();
client.BaseAddress = new Uri(serverUrl.TrimEnd('/') + "/");
client.Timeout = TimeSpan.FromSeconds(30);
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)
{
using var client = CreateClient(serverUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
var response = await client.GetAsync("api/libraries");
if (!response.IsSuccessStatusCode)
return [];
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(
l["id"]?.Value<string>() ?? "",
l["name"]?.Value<string>() ?? "",
(l["folders"] as JArray ?? new JArray())
.Select(f => new Folder(
f["id"]?.Value<string>() ?? "",
f["fullPath"]?.Value<string>() ?? ""))
.ToList()))
.ToList();
}
public static async Task<bool> TestConnectionAsync(string serverUrl, string apiToken)
{
try
{
using var client = CreateClient(serverUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
client.Timeout = TimeSpan.FromSeconds(10);
var response = await client.GetAsync("api/libraries");
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
}
public static async Task<bool> UploadBookAsync(
string serverUrl,
string apiToken,
string libraryId,
string folderId,
string title,
string? author,
string? series,
IEnumerable<string> filePaths)
{
using var client = CreateClient(serverUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
client.Timeout = TimeSpan.FromMinutes(30);
using var form = new MultipartFormDataContent();
form.Add(new StringContent(title), "title");
if (!string.IsNullOrWhiteSpace(author))
form.Add(new StringContent(author), "author");
if (!string.IsNullOrWhiteSpace(series))
form.Add(new StringContent(series), "series");
form.Add(new StringContent(libraryId), "library");
form.Add(new StringContent(folderId), "folder");
int fileIndex = 0;
foreach (var path in filePaths.Where(File.Exists))
{
var fileContent = new StreamContent(File.OpenRead(path));
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
form.Add(fileContent, fileIndex.ToString(), Path.GetFileName(path));
fileIndex++;
}
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");
}

View File

@@ -0,0 +1,117 @@
using ApplicationServices;
using DataLayer;
using Dinah.Core.ErrorHandling;
using FileManager;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace FileLiberator;
public class UploadToAudiobookshelf : Processable, IProcessable<UploadToAudiobookshelf>
{
public override string Name => "Upload to Audiobookshelf";
public override bool Validate(LibraryBook libraryBook)
{
if (!Configuration.AudiobookshelfEnabled)
return false;
if (string.IsNullOrWhiteSpace(Configuration.AudiobookshelfServerUrl)
|| string.IsNullOrWhiteSpace(Configuration.AudiobookshelfApiToken)
|| string.IsNullOrWhiteSpace(Configuration.AudiobookshelfLibraryId)
|| string.IsNullOrWhiteSpace(Configuration.AudiobookshelfFolderId))
return false;
return libraryBook.Book.AudioExists;
}
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
OnBegin(libraryBook);
try
{
var files = GetFilesToUpload(libraryBook);
if (files.Count == 0)
{
OnStatusUpdate("No audio files found to upload");
return new StatusHandler { "No audio files found to upload" };
}
OnStatusUpdate($"Uploading {files.Count} file(s) to Audiobookshelf...");
var title = libraryBook.Book.TitleWithSubtitle;
var author = libraryBook.Book.AuthorNames;
var series = libraryBook.Book.SeriesNames();
var success = await AudiobookshelfApiService.UploadBookAsync(
Configuration.AudiobookshelfServerUrl!,
Configuration.AudiobookshelfApiToken!,
Configuration.AudiobookshelfLibraryId!,
Configuration.AudiobookshelfFolderId!,
title,
author,
series,
files);
if (success)
{
OnStatusUpdate("Upload to Audiobookshelf completed successfully");
return new StatusHandler();
}
else
{
OnStatusUpdate("Upload to Audiobookshelf failed");
return new StatusHandler { "Upload to Audiobookshelf failed" };
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error uploading {Book} to Audiobookshelf", libraryBook.LogFriendly());
return new StatusHandler { $"Audiobookshelf upload error: {ex.Message}" };
}
finally
{
OnCompleted(libraryBook);
}
}
private static List<string> GetFilesToUpload(LibraryBook libraryBook)
{
var files = new List<string>();
var productId = libraryBook.Book.AudibleProductId;
// Get audio files from cache
var audioFiles = FilePathCache.GetFiles(productId)
.Where(f => f.fileType == FileType.Audio && File.Exists(f.path))
.Select(f => (string)f.path)
.Distinct()
.ToList();
files.AddRange(audioFiles);
// Also look for cover art in the same directory as the first audio file
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();
if (coverFile is not null)
files.Add(coverFile);
}
}
return files;
}
public static UploadToAudiobookshelf Create(Configuration config) => new() { Configuration = config };
private UploadToAudiobookshelf() { }
}

View File

@@ -0,0 +1,79 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
x:DataType="vm:AudiobookshelfSettingsVM"
x:Class="LibationAvalonia.Controls.Settings.Audiobookshelf">
<StackPanel Margin="6" Spacing="0">
<CheckBox IsChecked="{Binding Enabled, Mode=TwoWay}"
Content="Automatically upload downloaded books to Audiobookshelf"
Margin="0,0,0,10" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<!-- Server URL -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="Server URL"
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" />
<!-- API Token -->
<TextBlock Grid.Row="1" Grid.Column="0"
Text="API Token"
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" />
<!-- Connect Button -->
<Button Grid.Row="2" Grid.Column="1"
Content="Connect / Refresh"
Command="{Binding ConnectCommand}"
IsEnabled="{Binding !IsConnecting}"
HorizontalAlignment="Left"
HorizontalContentAlignment="Center"
MinWidth="150"
MinHeight="34"
Margin="0,0,0,10" />
<!-- Status -->
<ProgressBar Grid.Row="3" Grid.Column="1"
IsIndeterminate="True"
IsVisible="{Binding IsConnecting}"
Margin="0,0,0,6" />
<TextBlock Grid.Row="4" 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"
VerticalAlignment="Center"
Margin="0,0,10,7" />
<ComboBox Grid.Row="5" Grid.Column="1"
ItemsSource="{Binding LibraryNames}"
SelectedIndex="{Binding SelectedLibraryIndex, Mode=TwoWay}"
Margin="0,0,0,7" />
<!-- Folder -->
<TextBlock Grid.Row="6" Grid.Column="0"
Text="Folder"
VerticalAlignment="Center"
Margin="0,0,10,0" />
<ComboBox Grid.Row="6" Grid.Column="1"
ItemsSource="{Binding FolderNames}"
SelectedIndex="{Binding SelectedFolderIndex, Mode=TwoWay}" />
</Grid>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,18 @@
using Avalonia.Controls;
using LibationAvalonia.ViewModels.Settings;
using LibationFileManager;
namespace LibationAvalonia.Controls.Settings;
public partial class Audiobookshelf : UserControl
{
public Audiobookshelf()
{
InitializeComponent();
if (Design.IsDesignMode)
{
DataContext = new AudiobookshelfSettingsVM(Configuration.CreateMockInstance());
}
}
}

View File

@@ -80,6 +80,14 @@
<settings:Audio DataContext="{Binding AudioSettings, Mode=OneTime}" />
</ScrollViewer>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Text="Audiobookshelf"/>
</TabItem.Header>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<settings:Audiobookshelf DataContext="{Binding AudiobookshelfSettings, Mode=OneTime}" />
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</Window>

View File

@@ -0,0 +1,227 @@
using ApplicationServices;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
namespace LibationAvalonia.ViewModels.Settings;
public class AudiobookshelfSettingsVM : ViewModelBase
{
private readonly Configuration config;
private bool enabled;
private string serverUrl = "";
private string apiToken = "";
private string statusText = "";
private bool statusIsError;
private List<AudiobookshelfApiService.Library> libraries = [];
private int selectedLibraryIndex = -1;
private int selectedFolderIndex = -1;
private bool isConnecting;
private ObservableCollection<string> libraryNames = new();
private ObservableCollection<string> folderNames = new();
public AudiobookshelfSettingsVM(Configuration config)
{
this.config = config;
enabled = config.AudiobookshelfEnabled;
serverUrl = config.AudiobookshelfServerUrl ?? "";
apiToken = config.AudiobookshelfApiToken ?? "";
ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync);
_ = RestoreLibrariesAsync();
}
public bool Enabled
{
get => enabled;
set => this.RaiseAndSetIfChanged(ref enabled, value);
}
public string ServerUrl
{
get => serverUrl;
set => this.RaiseAndSetIfChanged(ref serverUrl, value);
}
public string ApiToken
{
get => apiToken;
set => this.RaiseAndSetIfChanged(ref apiToken, value);
}
public string StatusText
{
get => statusText;
set => this.RaiseAndSetIfChanged(ref statusText, value);
}
public bool StatusIsError
{
get => statusIsError;
set => this.RaiseAndSetIfChanged(ref statusIsError, value);
}
public List<AudiobookshelfApiService.Library> Libraries
{
get => libraries;
set
{
this.RaiseAndSetIfChanged(ref libraries, value);
UpdateLibraryNames();
}
}
public ObservableCollection<string> LibraryNames
{
get => libraryNames;
set => this.RaiseAndSetIfChanged(ref libraryNames, value);
}
public int SelectedLibraryIndex
{
get => selectedLibraryIndex;
set
{
this.RaiseAndSetIfChanged(ref selectedLibraryIndex, value);
UpdateFolders();
}
}
public ObservableCollection<string> FolderNames
{
get => folderNames;
set => this.RaiseAndSetIfChanged(ref folderNames, value);
}
public int SelectedFolderIndex
{
get => selectedFolderIndex;
set => this.RaiseAndSetIfChanged(ref selectedFolderIndex, value);
}
public bool IsConnecting
{
get => isConnecting;
set => this.RaiseAndSetIfChanged(ref isConnecting, value);
}
public bool CanConnect => !IsConnecting;
public ICommand ConnectCommand { get; }
private void UpdateLibraryNames()
{
LibraryNames.Clear();
foreach (var lib in Libraries)
LibraryNames.Add($"{lib.Name} ({lib.Id})");
}
private void UpdateFolders()
{
FolderNames.Clear();
SelectedFolderIndex = -1;
if (SelectedLibraryIndex < 0 || SelectedLibraryIndex >= Libraries.Count)
return;
var library = Libraries[SelectedLibraryIndex];
foreach (var folder in library.Folders)
FolderNames.Add($"{folder.FullPath} ({folder.Id})");
if (FolderNames.Count > 0)
SelectedFolderIndex = 0;
}
private async Task ConnectAsync()
{
IsConnecting = true;
StatusText = "Connecting...";
StatusIsError = false;
try
{
if (string.IsNullOrWhiteSpace(ServerUrl) || string.IsNullOrWhiteSpace(ApiToken))
{
StatusText = "Please enter both server URL and API token.";
StatusIsError = true;
return;
}
var libs = await AudiobookshelfApiService.GetLibrariesAsync(ServerUrl.Trim(), ApiToken.Trim());
if (libs.Count == 0)
{
StatusText = "Could not connect or no libraries found. Check your settings.";
StatusIsError = true;
return;
}
Libraries = libs;
SelectedLibraryIndex = 0;
StatusText = $"Connected. Found {libs.Count} library(ies).";
StatusIsError = false;
}
catch (System.Exception ex)
{
StatusText = $"Connection failed: {ex.Message}";
StatusIsError = true;
}
finally
{
IsConnecting = false;
}
}
private async Task RestoreLibrariesAsync()
{
try
{
if (string.IsNullOrWhiteSpace(config.AudiobookshelfServerUrl) || string.IsNullOrWhiteSpace(config.AudiobookshelfApiToken))
return;
var libs = await AudiobookshelfApiService.GetLibrariesAsync(config.AudiobookshelfServerUrl, config.AudiobookshelfApiToken);
Libraries = libs;
var savedLibIndex = libs.FindIndex(l => l.Id == config.AudiobookshelfLibraryId);
if (savedLibIndex >= 0)
{
SelectedLibraryIndex = savedLibIndex;
var library = libs[savedLibIndex];
var savedFolderIndex = library.Folders.FindIndex(f => f.Id == config.AudiobookshelfFolderId);
if (savedFolderIndex >= 0)
SelectedFolderIndex = savedFolderIndex;
}
}
catch
{
// Silently fail; user can manually reconnect
}
}
public void SaveSettings(Configuration config)
{
config.AudiobookshelfEnabled = Enabled;
config.AudiobookshelfServerUrl = ServerUrl.Trim();
config.AudiobookshelfApiToken = ApiToken.Trim();
if (SelectedLibraryIndex >= 0 && Libraries.Count > SelectedLibraryIndex)
config.AudiobookshelfLibraryId = Libraries[SelectedLibraryIndex].Id;
else
config.AudiobookshelfLibraryId = null;
if (SelectedLibraryIndex >= 0 && SelectedFolderIndex >= 0)
{
var library = Libraries[SelectedLibraryIndex];
if (library.Folders.Count > SelectedFolderIndex)
config.AudiobookshelfFolderId = library.Folders[SelectedFolderIndex].Id;
else
config.AudiobookshelfFolderId = null;
}
else
config.AudiobookshelfFolderId = null;
}
}

View File

@@ -10,12 +10,14 @@ public class SettingsVM
ImportSettings = new ImportSettingsVM(config);
DownloadDecryptSettings = new DownloadDecryptSettingsVM(config);
AudioSettings = new AudioSettingsVM(config);
AudiobookshelfSettings = new AudiobookshelfSettingsVM(config);
}
public ImportantSettingsVM ImportantSettings { get; private set; }
public ImportSettingsVM ImportSettings { get; private set; }
public DownloadDecryptSettingsVM DownloadDecryptSettings { get; private set; }
public AudioSettingsVM AudioSettings { get; private set; }
public AudiobookshelfSettingsVM AudiobookshelfSettings { get; private set; }
public void SaveSettings(Configuration config)
{
@@ -23,5 +25,6 @@ public class SettingsVM
ImportSettings.SaveSettings(config);
DownloadDecryptSettings.SaveSettings(config);
AudioSettings.SaveSettings(config);
AudiobookshelfSettings.SaveSettings(config);
}
}

View File

@@ -318,6 +318,23 @@ public partial class Configuration
//[Description("Request Spatial Audio")]
public bool RequestSpatial { get => false; set { } } // { get => GetNonString(defaultValue: true); set => SetNonString(value); }
#region Audiobookshelf
[Description("Automatically upload downloaded books to Audiobookshelf")]
public bool AudiobookshelfEnabled { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Audiobookshelf server URL")]
public string? AudiobookshelfServerUrl { get => GetString(); set => SetString(value); }
[Description("Audiobookshelf API token")]
public string? AudiobookshelfApiToken { get => GetString(); set => SetString(value); }
[Description("Audiobookshelf library ID")]
public string? AudiobookshelfLibraryId { get => GetString(); set => SetString(value); }
[Description("Audiobookshelf folder ID")]
public string? AudiobookshelfFolderId { get => GetString(); set => SetString(value); }
#endregion
//[Description("Spatial audio codec:")]
public SpatialCodec SpatialAudioCodec { get => GetNonString(defaultValue: SpatialCodec.EC_3); set => SetNonString(value); }

View File

@@ -269,6 +269,7 @@ public class ProcessBookViewModel : ReactiveObject
public ProcessBookViewModel AddDownloadPdf() => AddProcessable<DownloadPdf>();
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
public ProcessBookViewModel AddUploadToAudiobookshelf() => AddProcessable<UploadToAudiobookshelf>();
public ProcessBookViewModel AddSimulateBadBookFailure() => AddProcessable<SimulateBadBookFailure>();
private ProcessBookViewModel AddProcessable<T>() where T : Processable, IProcessable<T>

View File

@@ -314,7 +314,7 @@ public class ProcessQueueViewModel : ReactiveObject
AddToQueue(procs);
ProcessBookViewModel Create(LibraryBook entry)
=> new ProcessBookViewModel(entry, config, _badBookSession).AddDownloadDecryptBook().AddDownloadPdf();
=> new ProcessBookViewModel(entry, config, _badBookSession).AddDownloadDecryptBook().AddDownloadPdf().AddUploadToAudiobookshelf();
}
private void AddConvertMp3(IList<LibraryBook> entries, Configuration config)

View File

@@ -0,0 +1,317 @@
using ApplicationServices;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
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.Checked = config.AudiobookshelfEnabled;
absUrlTb!.Text = config.AudiobookshelfServerUrl ?? "";
absTokenTb!.Text = config.AudiobookshelfApiToken ?? "";
absTokenTb.PasswordChar = '*';
ToggleAudiobookshelfControls(absEnabledCb.Checked);
// Try to restore saved selections if we have them
if (!string.IsNullOrWhiteSpace(config.AudiobookshelfLibraryId))
_ = 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;
}
private void absEnabledCb_CheckedChanged(object? sender, EventArgs e)
=> ToggleAudiobookshelfControls(absEnabledCb!.Checked);
private async void absConnectBtn_Click(object? sender, EventArgs e)
{
absConnectBtn!.Enabled = false;
absStatusLbl!.Text = "Connecting...";
absStatusLbl.ForeColor = System.Drawing.SystemColors.ControlText;
try
{
var url = absUrlTb!.Text.Trim();
var token = absTokenTb!.Text.Trim();
if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(token))
{
absStatusLbl.Text = "Please enter both server URL and API token.";
absStatusLbl.ForeColor = System.Drawing.Color.DarkRed;
return;
}
var libraries = await AudiobookshelfApiService.GetLibrariesAsync(url, token);
if (libraries.Count == 0)
{
absStatusLbl.Text = "Could not connect or no libraries found. Check your settings.";
absStatusLbl.ForeColor = System.Drawing.Color.DarkRed;
return;
}
_absLibraries = libraries;
absLibraryCb!.Items.Clear();
absLibraryCb.Items.AddRange(libraries.Select(l => $"{l.Name} ({l.Id})").ToArray());
absFolderCb!.Items.Clear();
absFolderCb.SelectedIndex = -1;
absStatusLbl.Text = $"Connected. Found {libraries.Count} library(ies).";
absStatusLbl.ForeColor = System.Drawing.Color.DarkGreen;
ToggleAudiobookshelfControls(absEnabledCb!.Checked);
}
catch (Exception ex)
{
absStatusLbl.Text = $"Connection failed: {ex.Message}";
absStatusLbl.ForeColor = System.Drawing.Color.DarkRed;
}
finally
{
absConnectBtn.Enabled = true;
}
}
private void absLibraryCb_SelectedIndexChanged(object? sender, EventArgs e)
{
absFolderCb!.Items.Clear();
absFolderCb.SelectedIndex = -1;
if (absLibraryCb!.SelectedIndex < 0 || _absLibraries.Count == 0)
return;
var library = _absLibraries[absLibraryCb.SelectedIndex];
foreach (var folder in library.Folders)
{
absFolderCb.Items.Add($"{folder.FullPath} ({folder.Id})");
}
if (library.Folders.Count > 0)
absFolderCb.SelectedIndex = 0;
}
private async Task RestoreLibraryAndFolderAsync(Configuration config)
{
try
{
if (string.IsNullOrWhiteSpace(config.AudiobookshelfServerUrl) || string.IsNullOrWhiteSpace(config.AudiobookshelfApiToken))
return;
var libraries = await AudiobookshelfApiService.GetLibrariesAsync(config.AudiobookshelfServerUrl, config.AudiobookshelfApiToken);
_absLibraries = libraries;
absLibraryCb!.Items.Clear();
absLibraryCb.Items.AddRange(libraries.Select(l => $"{l.Name} ({l.Id})").ToArray());
var savedLibIndex = libraries.FindIndex(l => l.Id == config.AudiobookshelfLibraryId);
if (savedLibIndex >= 0)
{
absLibraryCb.SelectedIndex = savedLibIndex;
var library = libraries[savedLibIndex];
var savedFolderIndex = library.Folders.FindIndex(f => f.Id == config.AudiobookshelfFolderId);
if (savedFolderIndex >= 0)
absFolderCb!.SelectedIndex = savedFolderIndex;
}
ToggleAudiobookshelfControls(absEnabledCb!.Checked);
}
catch
{
// Silently fail; user can manually reconnect
}
}
private void Save_Audiobookshelf(Configuration config)
{
config.AudiobookshelfEnabled = absEnabledCb!.Checked;
config.AudiobookshelfServerUrl = absUrlTb!.Text.Trim();
config.AudiobookshelfApiToken = absTokenTb!.Text.Trim();
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)
{
var library = _absLibraries[absLibraryCb.SelectedIndex];
if (library.Folders.Count > absFolderCb.SelectedIndex)
config.AudiobookshelfFolderId = library.Folders[absFolderCb.SelectedIndex].Id;
else
config.AudiobookshelfFolderId = null;
}
else
config.AudiobookshelfFolderId = null;
}
}

View File

@@ -32,6 +32,7 @@ public partial class SettingsDialog : Form
Load_ImportLibrary(config);
Load_DownloadDecrypt(config);
Load_AudioSettings(config);
Load_Audiobookshelf(config);
}
private static void editTemplate(ITemplateEditor template, TextBox textBox)
@@ -47,6 +48,7 @@ public partial class SettingsDialog : Form
Save_ImportLibrary(config);
Save_DownloadDecrypt(config);
Save_AudioSettings(config);
Save_Audiobookshelf(config);
this.DialogResult = DialogResult.OK;
this.Close();