mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-07-29 16:36:00 -04:00
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:
131
Source/ApplicationServices/AudiobookshelfApiService.cs
Normal file
131
Source/ApplicationServices/AudiobookshelfApiService.cs
Normal 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");
|
||||
}
|
||||
117
Source/FileLiberator/UploadToAudiobookshelf.cs
Normal file
117
Source/FileLiberator/UploadToAudiobookshelf.cs
Normal 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() { }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
317
Source/LibationWinForms/Dialogs/SettingsDialog.Audiobookshelf.cs
Normal file
317
Source/LibationWinForms/Dialogs/SettingsDialog.Audiobookshelf.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user