Compare commits

...

32 Commits

Author SHA1 Message Date
Robert McRackan
54d157d244 another tag fail. incr ver 2023-06-11 17:07:03 -04:00
Robert McRackan
a4dfdf80e4 Merge branch 'master' of https://github.com/rmcrackan/Libation 2023-06-11 17:03:55 -04:00
Robert McRackan
d8c90bc745 incr ver 2023-06-11 17:03:35 -04:00
Mbucari
46accddd2d Merge pull request #623 from Mbucari/master
Redesign query sanitizer (#618)
2023-06-11 11:51:13 -06:00
Mbucari
f40ecbc07e Merge branch 'rmcrackan:master' into master 2023-06-11 11:33:28 -06:00
Mbucari
536982cb5f Remove obsolete code 2023-06-11 09:44:30 -06:00
Mbucari
ea3d96329b Add query sanitization unit tests 2023-06-11 09:44:21 -06:00
rmcrackan
e87fcbb16f Update Settings documentation 2023-06-11 10:04:00 -04:00
Mbucari
541cf79b6f Redesign query sanitizer (#618) 2023-06-10 15:08:50 -06:00
Robert McRackan
55fa82f92e New incr ver. Previous Tag attempt did generate builds; did not draft a new release 2023-06-09 11:49:59 -04:00
Robert McRackan
4a0c2b2180 Bug fix #618 2023-06-09 11:27:40 -04:00
Mbucari
c77fe5d561 Add Asin query tokenizer 2023-06-08 14:23:39 -06:00
Robert McRackan
359d082ffd incr ver 2023-06-03 15:06:12 -04:00
rmcrackan
017bdba404 Merge pull request #616 from Mbucari/master
Fix #612 and update Avalonia to v11-rc1
2023-06-03 15:04:56 -04:00
Mbucari
d4bf13b3fd Update Hangover Avalonia to v11-rc1 2023-06-03 00:30:02 -06:00
Mbucari
87b695b2de Merge branch 'rmcrackan:master' into master 2023-06-03 00:01:10 -06:00
Mbucari
222b16113e Update NamingTemplates.md 2023-06-03 00:00:01 -06:00
Mbucari
75c07c3209 Fix SavePodcastsToParentFolder setting (#612) 2023-06-02 23:54:32 -06:00
Mbucari
e640edee7f Use proper key name 2023-06-02 23:53:48 -06:00
Mbucari
6c48fc1f5e Update avalonia ro v11-RC1 2023-06-02 23:39:16 -06:00
Mbucari
e5708a382b Use new synchronous UI invoker 2023-06-02 23:21:55 -06:00
Robert McRackan
da9cb3371f incr ver 2023-05-23 13:06:09 -04:00
rmcrackan
91d0f8020e Merge pull request #606 from Mbucari/master
Corectly read and write locales
2023-05-23 13:04:35 -04:00
Mbucari
156726ca95 Corectly read and write locales 2023-05-23 09:41:28 -06:00
rmcrackan
3dad4c194b Update README.md 2023-05-20 23:12:23 -04:00
Mbucari
6025a7538a Merge pull request #604 from Mbucari/master
Fix rpm upgrade
2023-05-19 16:39:04 -06:00
Mbucari
824f65baae Fix rpm upgrade 2023-05-19 16:37:00 -06:00
Mbucari
9372a7318b inc ver 2023-05-19 13:46:26 -06:00
Mbucari
ddd032c16d Fix null string in integer fields 2023-05-19 13:36:22 -06:00
Mbucari
9aaf523240 Update InstallOnMac.md 2023-05-19 13:07:33 -06:00
Mbucari
8cbdeb38fa Update InstallOnLinux.md 2023-05-19 13:05:29 -06:00
Mbucari
a9258a1811 Update GettingStarted.md 2023-05-19 13:01:25 -06:00
30 changed files with 323 additions and 333 deletions

View File

@@ -28,6 +28,15 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
In addition to the options that are enabled if you allow Libation to "fix up" the audiobook, it does the following:
* Adds the `TCOM` metadata tag for the narrators.
* Sets the `©gen` metadata tag for the genres.
* Unescapes the copyright symbol (replace `©` with `©`)
* Replaces the recording copyright `(P)` string with `℗`
* Replaces the chapter markers embedded in the aax file with the chapter markers retrieved from Audible's API.
* Sets the embedded cover art image with the 500x500 px cover art retrieved from Audible
### Command Line Interface
Libationcli.exe allows limited access to Libation's functionalities as a CLI.

View File

@@ -33,7 +33,7 @@ Classic is Windows only. It has an older look because it's built with older, dul
Extract the zip file to a folder and then run `Libation.exe` from inside of that folder. Do not put it in Program Files. The inability to edit files from there causes problems with configuration and updating.
* [Ubuntu Linux](InstallOnLinux.md)
* [Linux](InstallOnLinux.md)
* [MacOS](InstallOnMac.md)
### Create Accounts

View File

@@ -6,16 +6,24 @@
### Install and Run Libation on Ubuntu
New Libation releases are automatically packed into a debian package and are available from the Libation repository's releases page.
New Libation releases are automatically packed into .deb and .rpm package and are available from the Libation repository's releases page.
Run this command in your terminal to dowbnload and install Libation, replacing the url with the Latest Libation .deb package url:
```Console
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
sudo apt install ./libation.deb
```
Run this command in your terminal to dowbnload and install Libation, replacing the url with the latest Libation package url:
You should now see Libation among your applications.
- Debian
```Console
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
sudo apt install ./libation.deb
```
- Redhat and CentOS
```Console
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
sudo yum install ./libation.rpm
```
If your desktop uses gtk, you should now see Libation among your applications.
Additionally, you may launch Libation, LibationCli, and Hangover (the Libation recovery app) via the command line using 'libation, libationcli', and 'hangover' aliases respectively.

View File

@@ -7,6 +7,8 @@
# Run Libation on MacOS
This walkthrough should get you up and running with Libation on your Mac.
## Supports macOS 10.15 (Catalina) and above
## Install Libation
- Download the file from the latest release and extract it.

View File

@@ -63,6 +63,9 @@ Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) w
|\<if series-\>...\<-if series\>|Only include if part of a book series or podcast|Conditional|
|\<if podcast-\>...\<-if podcast\>|Only include if part of a podcast|Conditional|
|\<if bookseries-\>...\<-if bookseries\>|Only include if part of a book series|Conditional|
|\<if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional|
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
For example, <if podcast-\>\<series\>\<-if podcast\> will evaluate to the podcast's series name if the file is a podcast. For audiobooks that are not podcasts, that tag will be blank.

View File

@@ -49,12 +49,12 @@
* Customizable saved filters for common searches
* Open source
* Supports most regions: US, UK, Canada, Germany, France, Australia, Japan, India, and Spain
* Fully supported in Windows, Mac, and Linux
<a name="theBad"/>
### The bad
* Only fully supported in Windows. (Mac and Linux are in beta)
* Large file size
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows

View File

@@ -81,8 +81,6 @@ if test -f 'libcoreclrtraceptprovider.so'; then
rm 'libcoreclrtraceptprovider.so'
fi
touch appsettings.json
chmod 666 appsettings.json
install -m 666 libation_glass.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/libation.svg
install -m 666 Libation.desktop %{buildroot}%{_datadir}/applications/Libation.desktop
@@ -94,26 +92,33 @@ install * %{buildroot}%{_libdir}/%{name}/
%post
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
ln -s %{_libdir}/%{name}/LibationCli %{_bindir}/libationcli
if [ \$1 -eq 1 ] ; then
# Initial installation
touch %{_libdir}/%{name}/appsettings.json
chmod 666 %{_libdir}/%{name}/appsettings.json
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
ln -s %{_libdir}/%{name}/LibationCli %{_bindir}/libationcli
gtk-update-icon-cache -f %{_datadir}/icons/hicolor/
gtk-update-icon-cache -f %{_datadir}/icons/hicolor/
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
fi
fi
%postun
rm %{_bindir}/libation
rm %{_bindir}/hangover
rm %{_bindir}/libationcli
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
if [ \$1 -eq 0 ] ; then
# Uninstall
rm %{_bindir}/libation
rm %{_bindir}/hangover
rm %{_bindir}/libationcli
fi
%files
%{_datadir}/icons/hicolor/scalable/apps/libation.svg
%{_datadir}/applications/Libation.desktop
%{_libdir}/%{name}/appsettings.json" >> ~/rpmbuild/SPECS/libation.spec
%{_datadir}/applications/Libation.desktop" >> ~/rpmbuild/SPECS/libation.spec
cd "$BIN_DIR"

View File

@@ -190,7 +190,11 @@ namespace AaxDecrypter
//Write aax decryption key
string keyPath = Path.ChangeExtension(aaxPath, ".key");
FileUtility.SaferDelete(keyPath);
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
if (string.IsNullOrEmpty(DownloadOptions.AudibleIV))
await File.WriteAllTextAsync(keyPath, $"ActivationBytes={DownloadOptions.AudibleKey}");
else
await File.WriteAllTextAsync(keyPath, $"Key={DownloadOptions.AudibleKey}{Environment.NewLine}IV={DownloadOptions.AudibleIV}");
OnFileCreated(aaxPath);
OnFileCreated(keyPath);

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>10.3.0.1</Version>
<Version>10.3.7.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="6.0.0" />

View File

@@ -43,6 +43,9 @@ namespace AudibleUtilities
[JsonProperty("locale_code")]
public string LocaleCode { get; private set; }
[JsonProperty("with_username")]
public bool WithUsername { get; private set; }
[JsonProperty("activation_bytes")]
public string ActivationBytes { get; private set; }
@@ -68,7 +71,8 @@ namespace AudibleUtilities
}
[JsonIgnore] public ISystemDateTime SystemDateTime { get; } = new SystemDateTime();
[JsonIgnore] public Locale Locale => Localization.Get(LocaleCode);
[JsonIgnore]
public Locale Locale => Localization.Locales.Where(l => l.WithUsername == WithUsername).Single(l => l.CountryCode == LocaleCode);
[JsonIgnore] public string DeviceSerialNumber => DeviceInfo.DeviceSerialNumber;
[JsonIgnore] public string DeviceType => DeviceInfo.DeviceType;
[JsonIgnore] public string AmazonAccountId => CustomerInfo.UserId;
@@ -177,6 +181,7 @@ namespace AudibleUtilities
DevicePrivateKey = account.IdentityTokens.PrivateKey,
AccessTokenExpires = account.IdentityTokens.ExistingAccessToken.Expires,
LocaleCode = account.Locale.CountryCode,
WithUsername = account.Locale.WithUsername,
RefreshToken = account.IdentityTokens.RefreshToken.Value,
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
WebsiteCookies = new(account.IdentityTokens.Cookies),

View File

@@ -25,8 +25,7 @@ namespace FileLiberator
if (seriesParent is not null)
{
var baseDir = Templates.Folder.GetFilename(seriesParent.ToDto(), AudibleFileStorage.BooksDirectory, "");
return Templates.Folder.GetFilename(libraryBook.ToDto(), baseDir, "");
return Templates.Folder.GetFilename(seriesParent.ToDto(), AudibleFileStorage.BooksDirectory, "");
}
}
}

View File

@@ -41,7 +41,8 @@ namespace FileLiberator
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
SeriesNumber = (int?)libraryBook.Book.SeriesLink.FirstOrDefault()?.Index,
IsPodcast = libraryBook.Book.IsEpisodeChild(),
IsPodcastParent = libraryBook.Book.IsEpisodeParent(),
IsPodcast = libraryBook.Book.IsEpisodeChild() || libraryBook.Book.IsEpisodeParent(),
BitRate = libraryBook.Book.AudioFormat.Bitrate,
SampleRate = libraryBook.Book.AudioFormat.SampleRate,

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
<TrimMode>copyused</TrimMode>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
@@ -66,13 +67,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview8" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />

View File

@@ -28,8 +28,6 @@ namespace LibationAvalonia
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
public static IAssetLoader AssetLoader { get; private set; }
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
@@ -37,7 +35,6 @@ namespace LibationAvalonia
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
AssetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
}
public static Task<List<DataLayer.LibraryBook>> LibraryTask;

View File

@@ -10,9 +10,9 @@ using System.Windows.Input;
namespace LibationAvalonia.Controls
{
public partial class LinkLabel : TextBlock, IStyleable, ICommandSource
public partial class LinkLabel : TextBlock, ICommandSource
{
Type IStyleable.StyleKey => typeof(LinkLabel);
protected override Type StyleKeyOverride => typeof(LinkLabel);
public static readonly StyledProperty<ICommand> CommandProperty =
AvaloniaProperty.Register<LinkLabel, ICommand>(nameof(Command), enableDataValidation: true);

View File

@@ -1,13 +1,13 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Styling;
using System;
namespace LibationAvalonia.Controls
{
public partial class WheelComboBox : ComboBox, IStyleable
public partial class WheelComboBox : ComboBox
{
Type IStyleable.StyleKey => typeof(ComboBox);
protected override Type StyleKeyOverride => typeof(ComboBox);
public WheelComboBox()
{
InitializeComponent();
@@ -16,9 +16,15 @@ namespace LibationAvalonia.Controls
{
var dir = Math.Sign(e.Delta.Y);
if (dir == 1 && SelectedIndex > 0)
{
SelectedIndex--;
e.Handled = true;
}
else if (dir == -1 && SelectedIndex < ItemCount - 1)
{
SelectedIndex++;
e.Handled = true;
}
base.OnPointerWheelChanged(e);
}

View File

@@ -70,13 +70,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview8" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview8" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,44 +0,0 @@
using Avalonia;
using Avalonia.Input;
namespace LibationAvalonia
{
internal class MacAccessKeyHandler : AccessKeyHandler
{
protected override void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key is Key.LWin or Key.RWin)
{
var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled };
base.OnPreviewKeyDown(sender, newArgs);
e.Handled = newArgs.Handled;
}
else if (e.Key is not Key.LeftAlt and not Key.RightAlt)
base.OnPreviewKeyDown(sender, e);
}
protected override void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
if (e.Key is Key.LWin or Key.RWin)
{
var newArgs = new KeyEventArgs { Key = Key.LeftAlt, Handled = e.Handled };
base.OnPreviewKeyUp(sender, newArgs);
e.Handled = newArgs.Handled;
}
else if (e.Key is not Key.LeftAlt and not Key.RightAlt)
base.OnPreviewKeyDown(sender, e);
}
protected override void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
{
var newArgs = new KeyEventArgs { Key = e.Key, Handled = e.Handled, KeyModifiers = KeyModifiers.Alt };
base.OnKeyDown(sender, newArgs);
e.Handled = newArgs.Handled;
}
else if (!e.KeyModifiers.HasFlag(KeyModifiers.Alt))
base.OnPreviewKeyDown(sender, e);
}
}
}

View File

@@ -61,12 +61,12 @@ namespace LibationAvalonia.ViewModels
#region Properties exposed to the view
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
public string Narrator { get => _narrator; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
public string Author { get => _author; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string Title { get => _title; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _title, value)); }
public int Progress { get => _progress; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
public string ETA { get => _eta; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
public string Narrator { get => _narrator; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
public string Author { get => _author; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string Title { get => _title; set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _title, value)); }
public int Progress { get => _progress; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
public string ETA { get => _eta; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Invoke(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
public bool IsDownloading => Status is ProcessBookStatus.Working;
public bool Queued => Status is ProcessBookStatus.Queued;

View File

@@ -45,11 +45,11 @@ namespace LibationAvalonia.ViewModels
private bool _progressBarVisible;
private decimal _speedLimit;
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Invoke(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
public bool AnyCompleted => CompletedCount > 0;
public bool AnyQueued => QueuedCount > 0;
public bool AnyErrors => ErrorCount > 0;
@@ -79,7 +79,7 @@ namespace LibationAvalonia.ViewModels
: _speedLimit > 1 ? 0.1m
: 0.01m;
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Invoke(() =>
{
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
this.RaisePropertyChanged();
@@ -106,7 +106,7 @@ namespace LibationAvalonia.ViewModels
public void WriteLine(string text)
{
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Invoke(() =>
LogEntries.Add(new()
{
LogDate = DateTime.Now,
@@ -183,7 +183,7 @@ namespace LibationAvalonia.ViewModels
public void AddToQueue(IEnumerable<ProcessBookViewModel> pbook)
{
Dispatcher.UIThread.Post(() =>
Dispatcher.UIThread.Invoke(() =>
{
Queue.Enqueue(pbook);
if (!Running)

View File

@@ -252,8 +252,8 @@ namespace LibationAvalonia.Views
var displayIndices = config.GridColumnsDisplayIndices;
var contextMenu = new ContextMenu();
contextMenu.MenuClosed += ContextMenu_MenuClosed;
contextMenu.ContextMenuOpening += ContextMenu_ContextMenuOpening;
contextMenu.Closed += ContextMenu_MenuClosed;
contextMenu.Opening += ContextMenu_ContextMenuOpening;
List<Control> menuItems = new();
contextMenu.ItemsSource = menuItems;

View File

@@ -248,7 +248,7 @@ namespace LibationAvalonia
private async Task displayControlAsync(TemplatedControl control)
{
await UIThread.InvokeAsync(() => control.IsEnabled = false);
await UIThread.InvokeAsync(MainForm.productsDisplay.Focus);
await UIThread.InvokeAsync(() => MainForm.productsDisplay.Focus());
await UIThread.InvokeAsync(() => flashControlAsync(control));
if (control is MenuItem menuItem) await UIThread.InvokeAsync(menuItem.Open);
await Task.Delay(500);

View File

@@ -22,6 +22,7 @@ namespace LibationFileManager
public string SeriesName { get; set; }
public int? SeriesNumber { get; set; }
public bool IsSeries => !string.IsNullOrEmpty(SeriesName);
public bool IsPodcastParent { get; set; }
public bool IsPodcast { get; set; }
public int BitRate { get; set; }

View File

@@ -47,6 +47,7 @@ namespace LibationFileManager
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<if series-><-if series>", "<if series->...<-if series>");
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if podcast>", "<if podcast->...<-if podcast>");
public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<if podcastparent-><-if podcastparent>", "<if podcastparent->...<-if podcastparent>");
public static TemplateTags IfBookseries { get; } = new TemplateTags("if bookseries", "Only include if part of a book series", "<if bookseries-><-if bookseries>", "<if bookseries->...<-if bookseries>");
}
}

View File

@@ -207,13 +207,13 @@ namespace LibationFileManager
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
{ TemplateTags.Series, lb => lb.SeriesName },
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
{ TemplateTags.SeriesNumber, lb => lb.IsPodcastParent ? null : lb.SeriesNumber },
{ TemplateTags.Language, lb => lb.Language },
//Don't allow formatting of LanguageShort
{ TemplateTags.LanguageShort, lb =>lb.Language, getLanguageShort },
{ TemplateTags.Bitrate, lb => lb.BitRate },
{ TemplateTags.SampleRate, lb => lb.SampleRate },
{ TemplateTags.Channels, lb => lb.Channels },
{ TemplateTags.Bitrate, lb => (int?)(lb.IsPodcastParent ? null : lb.BitRate) },
{ TemplateTags.SampleRate, lb => (int?)(lb.IsPodcastParent ? null : lb.SampleRate) },
{ TemplateTags.Channels, lb => (int?)(lb.IsPodcastParent ? null : lb.Channels) },
{ TemplateTags.Account, lb => lb.Account },
{ TemplateTags.Locale, lb => lb.Locale },
{ TemplateTags.YearPublished, lb => lb.YearPublished },
@@ -242,9 +242,14 @@ namespace LibationFileManager
private static readonly ConditionalTagCollection<LibraryBookDto> conditionalTags = new()
{
{ TemplateTags.IfSeries, lb => lb.IsSeries },
{ TemplateTags.IfPodcast, lb => lb.IsPodcast },
{ TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast },
{ TemplateTags.IfSeries, lb => lb.IsSeries || lb.IsPodcastParent },
{ TemplateTags.IfPodcast, lb => lb.IsPodcast || lb.IsPodcastParent },
{ TemplateTags.IfBookseries, lb => lb.IsSeries && !lb.IsPodcast && !lb.IsPodcastParent },
};
private static readonly ConditionalTagCollection<LibraryBookDto> folderConditionalTags = new()
{
{ TemplateTags.IfPodcastParent, lb => lb.IsPodcastParent }
};
#endregion
@@ -293,7 +298,8 @@ namespace LibationFileManager
public static string Name { get; }= "Folder Template";
public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate));
public static string DefaultTemplate { get; } = "<title short> [<id>]";
public static IEnumerable<TagCollection> TagCollections => new TagCollection[] { filePropertyTags, conditionalTags };
public static IEnumerable<TagCollection> TagCollections
=> new TagCollection[] { filePropertyTags, conditionalTags, folderConditionalTags };
public override IEnumerable<string> Errors
=> TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors;

View File

@@ -1,103 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace LibationSearchEngine
{
internal static partial class LuceneRegex
{
#region pattern pieces
// negative lookbehind: cannot be preceeded by an escaping \
const string NOT_ESCAPED = @"(?<!\\)";
// disallow spaces and lucene reserved characters
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
// define chars
// escape and concat
// create regex. also disallow spaces
private static char[] disallowedChars { get; } = new[] {
'+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\' };
private static string disallowedCharsEscaped { get; } = disallowedChars.Select(c => $@"\{c}").Aggregate((a, b) => a + b);
private static string WORD_CAPTURE { get; } = $@"([^\s{disallowedCharsEscaped}]+)";
// : with optional preceeding spaces. capture these so i don't accidentally replace a non-field name
const string FIELD_END = @"(\s*:)";
const string BEGIN_TAG = @"\[";
const string END_TAG = @"\]";
// space is forgiven at beginning and end of tag but not in the middle
// literal space character only. do NOT allow new lines, tabs, ...
const string OPTIONAL_SPACE_LITERAL = @"\u0020*";
#endregion
private static string tagPattern { get; } = NOT_ESCAPED + BEGIN_TAG + OPTIONAL_SPACE_LITERAL + WORD_CAPTURE + OPTIONAL_SPACE_LITERAL + END_TAG;
public static Regex TagRegex { get; } = new Regex(tagPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
private static string fieldPattern { get; } = NOT_ESCAPED + WORD_CAPTURE + FIELD_END;
public static Regex FieldRegex { get; } = new Regex(fieldPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
/// <summary>
/// auto-pad numbers to 8 char.s. This will match int.s and dates (yyyyMMdd)
/// positive look behind: beginning space { [ :
/// positive look ahead: end space ] }
/// </summary>
[GeneratedRegex(@"(?<=^|\s|\{|\[|:)(\d+\.?\d*)(?=$|\s|\]|\})", RegexOptions.Compiled)]
public static partial Regex NumbersRegex();
/// <summary>
/// proper bools are single keywords which are turned into keyword:True
/// if bordered by colons or inside brackets, they are not stand-alone bool keywords
/// the negative lookbehind and lookahead patterns prevent bugs where a bool keyword is also a user-defined tag:
/// [israted]
/// parseTag => tags:israted
/// replaceBools => tags:israted:True
/// or
/// [israted]
/// replaceBools => israted:True
/// parseTag => [israted:True]
/// also don't want to apply :True where the value already exists:
/// israted:false => israted:false:True
///
/// despite using parans, lookahead and lookbehind are zero-length assertions which do not capture. therefore the bool search keyword is still $1 since it's the first and only capture
/// </summary>
private static string boolPattern_parameterized { get; }
= @"
### IMPORTANT: 'ignore whitespace' is only partially honored in character sets
### - new lines are ok
### - ANY leading whitespace is treated like actual matching spaces :(
### can't begin with colon. incorrect syntax
### can't begin with open bracket: this signals the start of a tag
(?<! # begin negative lookbehind
[:\[] # char set: colon and open bracket, escaped
\s* # optional space
) # end negative lookbehind
\b # word boundary
({0}) # captured bool search keyword. this is the $1 reference used in regex.Replace
\b # word boundary
### can't end with colon. this signals that the bool's value already exists
### can't begin with close bracket: this signals the end of a tag
(?! # begin negative lookahead
\s* # optional space
[:\]] # char set: colon and close bracket, escaped
) # end negative lookahead
";
private static Dictionary<string, Regex> boolRegexDic { get; } = new Dictionary<string, Regex>();
public static Regex GetBoolRegex(string boolSearch)
{
if (boolRegexDic.TryGetValue(boolSearch, out var regex))
return regex;
var boolPattern = string.Format(boolPattern_parameterized, boolSearch);
regex = new Regex(boolPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase | RegexOptions.Compiled);
boolRegexDic.Add(boolSearch, regex);
return regex;
}
}
}

View File

@@ -0,0 +1,153 @@
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Analysis.Tokenattributes;
using System.Collections.Generic;
using System.Linq;
namespace LibationSearchEngine
{
internal static class QuerySanitizer
{
private static readonly HashSet<string> idTerms
= SearchEngine.idIndexRules.Keys
.Select(s => s.ToLowerInvariant())
.ToHashSet();
private static readonly HashSet<string> boolTerms
= SearchEngine.boolIndexRules.Keys
.Select(s => s.ToLowerInvariant())
.ToHashSet();
private static readonly HashSet<string> fieldTerms
= SearchEngine.stringIndexRules.Keys
.Union(SearchEngine.numberIndexRules.Keys)
.Select(s => s.ToLowerInvariant())
.Union(idTerms)
.Union(boolTerms)
.ToHashSet();
internal static string Sanitize(string searchString, StandardAnalyzer analyzer)
{
if (string.IsNullOrWhiteSpace(searchString))
return SearchEngine.ALL_QUERY;
// range operator " TO " and bool operators " AND " and " OR " must be uppercase
searchString
= searchString
.Replace(" to ", " TO ", System.StringComparison.OrdinalIgnoreCase)
.Replace(" and ", " AND ", System.StringComparison.OrdinalIgnoreCase)
.Replace(" or ", " OR ", System.StringComparison.OrdinalIgnoreCase);
using var tokenStream = analyzer.TokenStream(SearchEngine.ALL, new System.IO.StringReader(searchString));
var partList = new List<string>();
int previousEndOffset = 0;
bool previousIsBool = false, previousIsTags = false, previousIsAsin = false;
while (tokenStream.IncrementToken())
{
var term = tokenStream.GetAttribute<ITermAttribute>().Term;
var offset = tokenStream.GetAttribute<IOffsetAttribute>();
if (previousIsBool && !bool.TryParse(term, out _))
{
//The previous term was a boolean tag and this term is NOT a bool value
//Add the default ":True" bool and continue parsing the current term
partList.Add(":True");
previousIsBool = false;
}
//Add all text between the current token and the previous token
partList.Add(searchString.Substring(previousEndOffset, offset.StartOffset - previousEndOffset));
if (previousIsBool)
{
//The previous term was a boolean tag and this term is a bool value
addUnalteredToken(offset);
previousIsBool = false;
}
else if (previousIsAsin)
{
//The previous term was an ASIN field ID, so this term is an ASIN
partList.Add(term);
previousIsAsin = false;
}
else if (previousIsTags)
{
//This term is a tag. Do this check before checking if term is a defined field
//so that "tags:israted" does not parse as a bool
addUnalteredToken(offset);
previousIsTags = false;
}
else if (tryParseBlockTag(offset, partList, searchString, out var tagName))
{
//The term is a block tag. add it to the part list
partList.Add($"{SearchEngine.TAGS}:{tagName}");
}
else if (double.TryParse(term, out var num))
{
//Term is a number so pad it with zeros
partList.Add(num.ToLuceneString());
}
else if (fieldTerms.Contains(term))
{
//Term is a defined search field, add it.
//The StandardAnalyzer already converts all terms to lowercase
partList.Add(term);
previousIsBool = boolTerms.Contains(term);
previousIsAsin = idTerms.Contains(term);
previousIsTags = term == SearchEngine.TAGS;
}
else
{
//Term is any other user-defined constant value
addUnalteredToken(offset);
}
previousEndOffset = offset.EndOffset;
}
if (previousIsBool)
partList.Add(":True");
//Add ending non-token text
partList.Add(searchString.Substring(previousEndOffset, searchString.Length - previousEndOffset));
return string.Concat(partList);
//Add the full, unaltered token as well as all inter-token text
void addUnalteredToken(IOffsetAttribute offset) =>
partList.Add(searchString.Substring(offset.StartOffset, offset.EndOffset - offset.StartOffset));
}
private static bool tryParseBlockTag(IOffsetAttribute offset, List<string> partList, string searchString, out string tagName)
{
tagName = null;
if (partList.Count == 0) return false;
var previous = partList[^1].TrimEnd();
//cannot be preceeded by an escaping \
if (previous.Length == 0) return false;
if (previous[^1] != '[' || (previous.Length > 1 && previous[^2] == '\\')) return false;
var next = searchString.Substring(offset.EndOffset);
if (next.Length == 0 || !next.TrimStart().StartsWith(']')) return false;
tagName = searchString.Substring(offset.StartOffset, offset.EndOffset - offset.StartOffset);
//Only legal tag characters are letters, numbers and underscores
//Per DataLayer.UserDefinedItem.IllegalCharacterRegex()
foreach (var c in tagName)
{
if (!char.IsLetterOrDigit(c) && c != '_')
return false;
}
//Remove the leading '['
partList[^1] = previous[..^1];
//Ignore the trailing ']'
offset.SetOffset(offset.StartOffset, searchString.IndexOf(']', offset.EndOffset) + 1);
return true;
}
}
}

View File

@@ -7,6 +7,7 @@ using DataLayer;
using Dinah.Core;
using LibationFileManager;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Analysis.Tokenattributes;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Search;
@@ -31,18 +32,18 @@ namespace LibationSearchEngine
public const string ALL_NARRATOR_NAMES = "NarratorNames";
public const string ALL_SERIES_NAMES = "SeriesNames";
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
internal static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
new Dictionary<string, Func<LibraryBook, string>>
{
[nameof(Book.AudibleProductId)] = lb => lb.Book.AudibleProductId,
["ProductId"] = lb => lb.Book.AudibleProductId,
["Id"] = lb => lb.Book.AudibleProductId,
["ASIN"] = lb => lb.Book.AudibleProductId
}
[nameof(Book.AudibleProductId)] = lb => lb.Book.AudibleProductId.ToLowerInvariant(),
["ProductId"] = lb => lb.Book.AudibleProductId.ToLowerInvariant(),
["Id"] = lb => lb.Book.AudibleProductId.ToLowerInvariant(),
["ASIN"] = lb => lb.Book.AudibleProductId.ToLowerInvariant()
}
);
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> stringIndexRules { get; }
internal static ReadOnlyDictionary<string, Func<LibraryBook, string>> stringIndexRules { get; }
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
new Dictionary<string, Func<LibraryBook, string>>
{
@@ -74,7 +75,7 @@ namespace LibationSearchEngine
}
);
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> numberIndexRules { get; }
internal static ReadOnlyDictionary<string, Func<LibraryBook, string>> numberIndexRules { get; }
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
new Dictionary<string, Func<LibraryBook, string>>
{
@@ -90,15 +91,15 @@ namespace LibationSearchEngine
["UserRating"] = lb => userOverallRating(lb.Book),
["MyRating"] = lb => userOverallRating(lb.Book),
[nameof(LibraryBook.DateAdded)] = lb => lb.DateAdded.ToLuceneString(),
[nameof(Book.DatePublished)] = lb => lb.Book.DatePublished?.ToLuceneString(),
[nameof(LibraryBook.DateAdded)] = lb => lb.DateAdded.ToLuceneString(),
[nameof(Book.DatePublished)] = lb => lb.Book.DatePublished?.ToLuceneString() ?? "",
["LastDownload"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(),
["LastDownload"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString(),
["LastDownloaded"] = lb => lb.Book.UserDefinedItem.LastDownloaded.ToLuceneString()
}
);
private static ReadOnlyDictionary<string, Func<LibraryBook, bool>> boolIndexRules { get; }
internal static ReadOnlyDictionary<string, Func<LibraryBook, bool>> boolIndexRules { get; }
= new ReadOnlyDictionary<string, Func<LibraryBook, bool>>(
new Dictionary<string, Func<LibraryBook, bool>>
{
@@ -352,115 +353,34 @@ namespace LibationSearchEngine
#region search
public SearchResultSet Search(string searchString)
{
Serilog.Log.Logger.Debug("original search string: {@DebugInfo}", new { searchString });
searchString = FormatSearchQuery(searchString);
{
using var analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30);
Serilog.Log.Logger.Debug("original search string: {@DebugInfo}", new { searchString });
searchString = QuerySanitizer.Sanitize(searchString, analyzer);
Serilog.Log.Logger.Debug("formatted search string: {@DebugInfo}", new { searchString });
var results = generalSearch(searchString);
var results = generalSearch(searchString, analyzer);
Serilog.Log.Logger.Debug("Hit(s): {@DebugInfo}", new { count = results.Docs.Count() });
displayResults(results);
return results;
}
internal static string FormatSearchQuery(string searchString)
{
if (string.IsNullOrWhiteSpace(searchString))
return ALL_QUERY;
searchString = replaceBools(searchString);
searchString = parseTag(searchString);
// in ranges " TO " must be uppercase
searchString = searchString.Replace(" to ", " TO ");
searchString = padNumbers(searchString);
searchString = lowerFieldNames(searchString);
return searchString;
}
#region format query string
private static string parseTag(string tagSearchString)
{
var allMatches = LuceneRegex
.TagRegex
.Matches(tagSearchString)
.Cast<Match>()
.Select(a => a.ToString())
.ToList();
foreach (var match in allMatches)
tagSearchString = tagSearchString.Replace(
match,
TAGS + ":" + match.Trim('[', ']').Trim()
);
return tagSearchString;
}
private static string replaceBools(string searchString)
{
foreach (var boolSearch in boolIndexRules.Keys)
searchString =
LuceneRegex.GetBoolRegex(boolSearch)
.Replace(searchString, @"$1:True");
return searchString;
}
private static string padNumbers(string searchString)
{
var matches = LuceneRegex
.NumbersRegex()
.Matches(searchString)
.Cast<Match>()
.OrderByDescending(m => m.Index);
foreach (var m in matches)
{
var replaceString = double.Parse(m.ToString()).ToLuceneString();
searchString = LuceneRegex.NumbersRegex().Replace(searchString, replaceString, 1, m.Index);
}
return searchString;
}
private static string lowerFieldNames(string searchString)
{
// fields are case specific
var allMatches = LuceneRegex
.FieldRegex
.Matches(searchString)
.Cast<Match>()
.Select(a => a.ToString())
.ToList();
foreach (var match in allMatches)
searchString = searchString.Replace(match, match.ToLowerInvariant());
return searchString;
}
#endregion
private SearchResultSet generalSearch(string searchString)
private SearchResultSet generalSearch(string searchString, StandardAnalyzer analyzer)
{
var defaultField = ALL;
using var index = getIndex();
using var searcher = new IndexSearcher(index);
using var analyzer = new StandardAnalyzer(Version);
var query = analyzer.GetQuery(defaultField, searchString);
var query = analyzer.GetQuery(defaultField, searchString);
// lucene doesn't allow only negations. eg this returns nothing:
// -tags:hidden
// work arounds: https://kb.ucla.edu/articles/pure-negation-query-in-lucene
// HOWEVER, doing this to any other type of query can cause EVERYTHING to be a match unless "Occur" is carefully set
// this should really check that all leaf nodes are MUST_NOT
if (query is BooleanQuery boolQuery)
// lucene doesn't allow only negations. eg this returns nothing:
// -tags:hidden
// work arounds: https://kb.ucla.edu/articles/pure-negation-query-in-lucene
// HOWEVER, doing this to any other type of query can cause EVERYTHING to be a match unless "Occur" is carefully set
// this should really check that all leaf nodes are MUST_NOT
if (query is BooleanQuery boolQuery)
{
var occurs = getOccurs_recurs(boolQuery);
if (occurs.Any() && occurs.All(o => o == Occur.MUST_NOT))

View File

@@ -29,11 +29,11 @@ namespace LinuxConfigApp
//only run the auto upgrader if the current app was installed from the
//.deb or .rpm package. Try to detect this by checking if the symlink exists.
public bool CanUpgrade => Directory.Exists("/usr/lib/libation");
public bool CanUpgrade => File.Exists("/bin/libation");
public void InstallUpgrade(string upgradeBundle)
{
if (File.Exists("/bin/yum"))
RunAsRoot("yum", $"install '{upgradeBundle}'");
RunAsRoot("yum", $"install -y '{upgradeBundle}'");
else
RunAsRoot("apt", $"install '{upgradeBundle}'");
}

View File

@@ -10,6 +10,7 @@ using Dinah.Core;
using FluentAssertions;
using FluentAssertions.Common;
using LibationSearchEngine;
using Lucene.Net.Analysis.Standard;
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -31,6 +32,7 @@ namespace SearchEngineTests
// tag surrounded by spaces
[DataRow("[foo]", "tags:foo")]
[DataRow(" [foo]", " tags:foo")]
[DataRow(" [ foo ]", " tags:foo")]
[DataRow("[foo] ", "tags:foo ")]
[DataRow(" [foo] ", " tags:foo ")]
[DataRow("-[foo]", "-tags:foo")]
@@ -51,15 +53,25 @@ namespace SearchEngineTests
[DataRow("-israted ", "-israted:True ")]
[DataRow(" -israted ", " -israted:True ")]
//ID Tags to lowercase and not parsed as numbers
[DataRow("id:0000000123", "id:0000000123")]
[DataRow("id:B000000123", "id:b000000123")]
[DataRow("ASIN:B000000123", "asin:b000000123")]
[DataRow("AudibleProductId:B000000123", "audibleproductid:b000000123")]
[DataRow("ProductId:B000000123", "productid:b000000123")]
// bool keyword. Append :True
[DataRow("israted", "israted:True")]
// bool keyword with [:bool]. Do not add :True
[DataRow("israted:True", "israted:True")]
[DataRow("isRated:false", "israted:false")]
[DataRow("liberated AND isRated:false", "liberated:True AND israted:false")]
// tag which happens to be a bool keyword >> parse as tag
[DataRow("[israted]", "tags:israted")]
[DataRow("[tags] [israted] [tags] [tags] [isliberated] [israted] ", "tags:tags tags:israted tags:tags tags:tags tags:isliberated tags:israted ")]
[DataRow("[tags][israted]", "tags:tagstags:israted")]
// numbers with "to". TO all caps, numbers [8.2] format
[DataRow("1 to 10", "00000001.00 TO 00000010.00")]
@@ -72,6 +84,10 @@ namespace SearchEngineTests
[DataRow("-isRATED", "-israted:True")]
public void FormattingTest(string input, string output)
=> SearchEngine.FormatSearchQuery(input).Should().Be(output);
{
using var analyzer = new StandardAnalyzer(SearchEngine.Version);
QuerySanitizer.Sanitize(input, analyzer).Should().Be(output);
}
}
}