mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-10 23:08:43 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d6c742ae9 | ||
|
|
933f663d22 | ||
|
|
0c55f278a4 | ||
|
|
3f567ee82e | ||
|
|
8dc912c11d | ||
|
|
f1b4e2a17d | ||
|
|
630cfdeab3 | ||
|
|
7029409792 | ||
|
|
d0727b5a85 | ||
|
|
9f52ad5e0a | ||
|
|
501ae643f7 | ||
|
|
400074170e | ||
|
|
17103ed066 | ||
|
|
b6b29309c9 | ||
|
|
a04538710f | ||
|
|
01f6f5c137 | ||
|
|
b1a37cbd8c | ||
|
|
8c59e1280b | ||
|
|
00339127aa | ||
|
|
5935b40b60 | ||
|
|
6cfd2dea96 | ||
|
|
7d3a39c693 | ||
|
|
6e7a4ea475 | ||
|
|
3479dbc3f0 | ||
|
|
9309aea6d9 |
6
.github/workflows/build-linux.yml
vendored
6
.github/workflows/build-linux.yml
vendored
@@ -15,10 +15,6 @@ on:
|
||||
description: 'Skip running unit tests'
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
version:
|
||||
description: "The Libation version number"
|
||||
value: ${{ jobs.build.outputs.version }}
|
||||
|
||||
env:
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
@@ -27,8 +23,6 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [Linux, MacOS]
|
||||
|
||||
8
.github/workflows/build-windows.yml
vendored
8
.github/workflows/build-windows.yml
vendored
@@ -15,10 +15,6 @@ on:
|
||||
description: 'Skip running unit tests'
|
||||
required: false
|
||||
default: true
|
||||
outputs:
|
||||
version:
|
||||
description: "The Libation version number"
|
||||
value: ${{ jobs.build.outputs.version }}
|
||||
|
||||
env:
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
@@ -27,8 +23,6 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [Windows]
|
||||
@@ -74,7 +68,7 @@ jobs:
|
||||
- name: Zip artifact
|
||||
id: zip
|
||||
working-directory: ./Source/bin/Publish
|
||||
run: |
|
||||
run: |
|
||||
$dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
|
||||
$delfiles = @("libmp3lame.so", "ffmpegaac.so", "glass-with-glow_256.svg", "Libation.desktop")
|
||||
foreach ($file in $delfiles){ if (test-path $dir$file){ Remove-Item $dir$file } }
|
||||
|
||||
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@@ -15,11 +15,6 @@ on:
|
||||
description: 'Skip running unit tests'
|
||||
required: false
|
||||
default: true
|
||||
build_deb:
|
||||
type: boolean
|
||||
description: 'Build Debian package'
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
@@ -32,12 +27,4 @@ jobs:
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
with:
|
||||
version_override: ${{ inputs.version_override }}
|
||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||
|
||||
linux_deb:
|
||||
needs: [linux]
|
||||
if: inputs.build_deb
|
||||
uses: ./.github/workflows/build-deb.yml
|
||||
with:
|
||||
version: ${{ needs.linux.outputs.version }}
|
||||
|
||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# build-deb.yml
|
||||
# deb.yml
|
||||
# Reusable workflow that builds the Linux Debian package.
|
||||
---
|
||||
name: build_deb
|
||||
name: deb
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -21,15 +21,14 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@master
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: "${{ env.FILE_NAME }}.tar.gz"
|
||||
|
||||
- name: Build .deb
|
||||
id: deb
|
||||
run: |
|
||||
chmod +x ./.github/workflows/scripts/targz2deb.sh
|
||||
./.github/workflows/scripts/targz2deb.sh "${{ env.FILE_NAME }}.tar.gz" ${{ inputs.version }}
|
||||
./Scripts/targz2deb.sh "${{ env.FILE_NAME }}.tar.gz" ${{ inputs.version }}
|
||||
|
||||
- name: Publish .deb
|
||||
uses: actions/upload-artifact@v3
|
||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -33,10 +33,15 @@ jobs:
|
||||
with:
|
||||
version_override: ${{ needs.prerelease.outputs.version }}
|
||||
run_unit_tests: false
|
||||
build_deb: true
|
||||
|
||||
deb:
|
||||
needs: [prerelease,build]
|
||||
uses: ./.github/workflows/deb.yml
|
||||
with:
|
||||
version: ${{ needs.prerelease.outputs.version }}
|
||||
|
||||
release:
|
||||
needs: [prerelease,build]
|
||||
needs: [prerelease,build,deb]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
@@ -44,14 +49,11 @@ jobs:
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
- name: Release
|
||||
id: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: '${{ github.ref }}'
|
||||
release_name: 'Libation ${{ needs.prerelease.outputs.version }}'
|
||||
name: Libation ${{ needs.prerelease.outputs.version }}
|
||||
body: <Put a body here>
|
||||
draft: true
|
||||
prerelease: false
|
||||
@@ -61,5 +63,5 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
with:
|
||||
release_id: '${{ steps.create_release.outputs.id }}'
|
||||
release_id: '${{ steps.release.outputs.id }}'
|
||||
assets_path: ./artifacts
|
||||
|
||||
36
Documentation/Docker.md
Normal file
36
Documentation/Docker.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
### Setup
|
||||
In order to use the docker image, you'll need to provide it with a copy of the `AccountsSettings.json`, `Settings.json`, and `LibationContext.db` files. These files can usually be found in the Libation folder in your user's home directory. If you haven't run Libation yet, you'll need to launch it to generate these files and setup your accounts. Once you have them, copy these files to a new location, such as `/opt/libation/config`. Before using them we'll need to make a couple edits so that the filepaths referenced are correct when running from the docker image.
|
||||
|
||||
In Settings.json, make the following changes:
|
||||
* Change `Books` to `/data`
|
||||
* Change `InProgress` to `/tmp`
|
||||
|
||||
### Running
|
||||
Once the configuration files are copied and edited, the docker image can be run with the following command.
|
||||
```
|
||||
sudo docker run -d \
|
||||
-v /opt/libation/config:/config \
|
||||
-v /opt/libation/books:/data \
|
||||
--name libation \
|
||||
--restart=always \
|
||||
rmcrackan/libation
|
||||
```
|
||||
|
||||
By default the container will scan for new books every 30 minutes and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. Additionally, if you pass in `-1` it will scan and download books once and then exit.
|
||||
|
||||
```
|
||||
sudo docker run -d \
|
||||
-v /opt/libation/config:/config \
|
||||
-v /opt/libation/books:/data \
|
||||
-e SLEEP_TIME='10m' \
|
||||
--name libation \
|
||||
--restart=always \
|
||||
rmcrackan/libation
|
||||
```
|
||||
|
||||
@@ -38,3 +38,7 @@ Once Gatekeeper reenabled, you can open Libation again without it being blocked.
|
||||
Thanks [joseph-holland](https://github.com/rmcrackan/Libation/issues/327#issuecomment-1268993349)!
|
||||
|
||||
Report bugs to https://github.com/rmcrackan/Libation/issues
|
||||
|
||||
## Get Libation running on Mac
|
||||
|
||||
[Run Libation on MacOS](https://user-images.githubusercontent.com/37587114/213933357-983d8ede-2738-4b32-9c6e-40de21ff09c2.mp4)
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
- [Settings](Documentation/Advanced.md#settings)
|
||||
- [Custom File Naming](Documentation/Advanced.md#custom-file-naming)
|
||||
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
||||
- [Docker](Documentation/Docker.md)
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
0
.github/workflows/scripts/targz2deb.sh → Scripts/targz2deb.sh
Normal file → Executable file
0
.github/workflows/scripts/targz2deb.sh → Scripts/targz2deb.sh
Normal file → Executable file
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.5.0" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.5.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace AaxDecrypter
|
||||
public event EventHandler<AppleTags> RetrievedMetadata;
|
||||
|
||||
protected AaxFile AaxFile;
|
||||
protected Mp4Operation aaxConversion;
|
||||
|
||||
protected AaxcDownloadConvertBase(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions) { }
|
||||
@@ -101,9 +102,9 @@ namespace AaxDecrypter
|
||||
public override async Task CancelAsync()
|
||||
{
|
||||
IsCanceled = true;
|
||||
if (AaxFile != null)
|
||||
await AaxFile.CancelAsync();
|
||||
AaxFile?.Dispose();
|
||||
if (aaxConversion != null)
|
||||
await aaxConversion.CancelAsync();
|
||||
AaxFile?.Close();
|
||||
CloseInputFileStream();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,32 +133,37 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
|
||||
try
|
||||
{
|
||||
ConversionResult result;
|
||||
|
||||
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
|
||||
result = await ConvertToMultiMp4a(splitChapters);
|
||||
aaxConversion = ConvertToMultiMp4a(splitChapters);
|
||||
else
|
||||
result = await ConvertToMultiMp3(splitChapters);
|
||||
aaxConversion = ConvertToMultiMp3(splitChapters);
|
||||
|
||||
return result == ConversionResult.NoErrorsDetected;
|
||||
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
await aaxConversion;
|
||||
|
||||
if (aaxConversion.IsCompletedSuccessfully)
|
||||
moveMoovToBeginning(workingFileStream?.Name);
|
||||
|
||||
return aaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "AAXClean Error");
|
||||
workingFileStream?.Close();
|
||||
FileUtility.SaferDelete(workingFileStream.Name);
|
||||
if (workingFileStream?.Name is not null)
|
||||
FileUtility.SaferDelete(workingFileStream.Name);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
if (aaxConversion is not null)
|
||||
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
Step_DownloadAudiobook_End(zeroProgress);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<ConversionResult> ConvertToMultiMp4a(ChapterInfo splitChapters)
|
||||
private Mp4Operation ConvertToMultiMp4a(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
return AaxFile.ConvertToMultiMp4aAsync
|
||||
@@ -169,7 +174,7 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
);
|
||||
}
|
||||
|
||||
private Task<ConversionResult> ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||
private Mp4Operation ConvertToMultiMp3(ChapterInfo splitChapters)
|
||||
{
|
||||
var chapterCount = 0;
|
||||
return AaxFile.ConvertToMultiMp3Async
|
||||
@@ -194,12 +199,26 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
PartsTotal = splitChapters.Count,
|
||||
Title = newSplitCallback?.Chapter?.Title,
|
||||
};
|
||||
|
||||
moveMoovToBeginning(workingFileStream?.Name);
|
||||
|
||||
newSplitCallback.OutputFile = createOutputFileStream(props);
|
||||
newSplitCallback.TrackTitle = DownloadOptions.GetMultipartTitleName(props);
|
||||
newSplitCallback.TrackNumber = currentChapter;
|
||||
newSplitCallback.TrackCount = splitChapters.Count;
|
||||
}
|
||||
|
||||
private void moveMoovToBeginning(string filename)
|
||||
{
|
||||
if (DownloadOptions.OutputFormat is OutputFormat.M4b
|
||||
&& DownloadOptions.MoveMoovToBeginning
|
||||
&& filename is not null
|
||||
&& File.Exists(filename))
|
||||
{
|
||||
Mp4File.RelocateMoovAsync(filename).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
|
||||
{
|
||||
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using AAXClean;
|
||||
using AAXClean.Codecs;
|
||||
using FileManager;
|
||||
using Mpeg4Lib.Util;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
@@ -90,16 +91,29 @@ namespace AaxDecrypter
|
||||
var outputFile = File.Open(OutputFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
OnFileCreated(OutputFileName);
|
||||
|
||||
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
|
||||
try
|
||||
{
|
||||
ConversionResult decryptionResult = await decryptAsync(outputFile);
|
||||
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
|
||||
if (success)
|
||||
aaxConversion = decryptAsync(outputFile);
|
||||
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
await aaxConversion;
|
||||
|
||||
outputFile.Close();
|
||||
|
||||
if (aaxConversion.IsCompletedSuccessfully
|
||||
&& DownloadOptions.OutputFormat is OutputFormat.M4b
|
||||
&& DownloadOptions.MoveMoovToBeginning)
|
||||
{
|
||||
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
aaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
||||
aaxConversion.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
|
||||
await aaxConversion;
|
||||
}
|
||||
|
||||
if (aaxConversion.IsCompletedSuccessfully)
|
||||
base.OnFileCreated(OutputFileName);
|
||||
|
||||
return success;
|
||||
return aaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
@@ -110,13 +124,15 @@ namespace AaxDecrypter
|
||||
finally
|
||||
{
|
||||
outputFile.Close();
|
||||
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
if (aaxConversion is not null)
|
||||
aaxConversion.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
|
||||
|
||||
Step_DownloadAudiobook_End(zeroProgress);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<ConversionResult> decryptAsync(Stream outputFile)
|
||||
private Mp4Operation decryptAsync(Stream outputFile)
|
||||
=> DownloadOptions.OutputFormat == OutputFormat.Mp3 ?
|
||||
AaxFile.ConvertToMp3Async
|
||||
(
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace AaxDecrypter
|
||||
NAudio.Lame.LameConfig LameConfig { get; }
|
||||
bool Downsample { get; }
|
||||
bool MatchSourceBitrate { get; }
|
||||
bool MoveMoovToBeginning { get; }
|
||||
string GetMultipartFileName(MultiConvertFileProperties props);
|
||||
string GetMultipartTitleName(MultiConvertFileProperties props);
|
||||
Task<string> SaveClipsAndBookmarks(string fileName);
|
||||
|
||||
@@ -10,6 +10,6 @@ namespace AaxDecrypter
|
||||
public int PartsPosition { get; set; }
|
||||
public int PartsTotal { get; set; }
|
||||
public string Title { get; set; }
|
||||
|
||||
public DateTime FileDate { get; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>9.0.1.1</Version>
|
||||
<Version>9.1.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="4.0.3" />
|
||||
|
||||
@@ -15,12 +15,12 @@ namespace FileLiberator
|
||||
public class ConvertToMp3 : AudioDecodable
|
||||
{
|
||||
public override string Name => "Convert to Mp3";
|
||||
private Mp4File m4bBook;
|
||||
|
||||
private Mp4Operation Mp4Operation;
|
||||
private TimeSpan bookDuration;
|
||||
private long fileSize;
|
||||
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
|
||||
|
||||
public override Task CancelAsync() => m4bBook?.CancelAsync() ?? Task.CompletedTask;
|
||||
public override Task CancelAsync() => Mp4Operation?.CancelAsync() ?? Task.CompletedTask;
|
||||
|
||||
public static bool ValidateMp3(LibraryBook libraryBook)
|
||||
{
|
||||
@@ -43,9 +43,9 @@ namespace FileLiberator
|
||||
var proposedMp3Path = Mp3FileName(m4bPath);
|
||||
if (File.Exists(proposedMp3Path) || !File.Exists(m4bPath)) continue;
|
||||
|
||||
m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read));
|
||||
m4bBook.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||
var m4bBook = await Task.Run(() => new Mp4File(m4bPath, FileAccess.Read));
|
||||
|
||||
bookDuration = m4bBook.Duration;
|
||||
fileSize = m4bBook.InputStream.Length;
|
||||
|
||||
OnTitleDiscovered(m4bBook.AppleTags.Title);
|
||||
@@ -66,20 +66,20 @@ namespace FileLiberator
|
||||
using var mp3File = File.OpenWrite(Path.GetTempFileName());
|
||||
try
|
||||
{
|
||||
var result = await m4bBook.ConvertToMp3Async(mp3File, lameConfig);
|
||||
Mp4Operation = m4bBook.ConvertToMp3Async(mp3File, lameConfig);
|
||||
Mp4Operation.ConversionProgressUpdate += M4bBook_ConversionProgressUpdate;
|
||||
await Mp4Operation;
|
||||
|
||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters);
|
||||
OnFileCreated(libraryBook, realMp3Path);
|
||||
|
||||
if (result == ConversionResult.Failed)
|
||||
{
|
||||
FileUtility.SaferDelete(mp3File.Name);
|
||||
}
|
||||
else if (result == ConversionResult.Cancelled)
|
||||
if (Mp4Operation.IsCanceled)
|
||||
{
|
||||
FileUtility.SaferDelete(mp3File.Name);
|
||||
return new StatusHandler { "Cancelled" };
|
||||
}
|
||||
else
|
||||
{
|
||||
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters, "mp3");
|
||||
OnFileCreated(libraryBook, realMp3Path);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -88,6 +88,9 @@ namespace FileLiberator
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Mp4Operation is not null)
|
||||
Mp4Operation.ConversionProgressUpdate -= M4bBook_ConversionProgressUpdate;
|
||||
|
||||
m4bBook.InputStream.Close();
|
||||
mp3File.Close();
|
||||
}
|
||||
@@ -102,14 +105,13 @@ namespace FileLiberator
|
||||
|
||||
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
var duration = m4bBook.Duration;
|
||||
var remainingSecsToProcess = (duration - e.ProcessPosition).TotalSeconds;
|
||||
var remainingSecsToProcess = (bookDuration - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingSecsToProcess / e.ProcessSpeed;
|
||||
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / duration.TotalSeconds;
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / bookDuration.TotalSeconds;
|
||||
|
||||
OnStreamingProgressChanged(
|
||||
new DownloadProgress
|
||||
|
||||
@@ -160,6 +160,7 @@ namespace FileLiberator
|
||||
AudibleKey = contentLic?.Voucher?.Key,
|
||||
AudibleIV = contentLic?.Voucher?.Iv,
|
||||
OutputFormat = outputFormat,
|
||||
MoveMoovToBeginning = config.MoveMoovToBeginning,
|
||||
TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio,
|
||||
RetainEncryptedFile = config.RetainAaxFile && encrypted,
|
||||
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace FileLiberator
|
||||
public bool MatchSourceBitrate { get; init; }
|
||||
public ReplacementCharacters ReplacementCharacters => Configuration.Instance.ReplacementCharacters;
|
||||
|
||||
public bool MoveMoovToBeginning { get; init; }
|
||||
|
||||
public string GetMultipartFileName(MultiConvertFileProperties props)
|
||||
=> Templates.ChapterFile.GetFilename(LibraryBookDto, props);
|
||||
|
||||
|
||||
@@ -27,11 +27,13 @@ namespace FileLiberator
|
||||
public static LibraryBookDto ToDto(this LibraryBook libraryBook) => new()
|
||||
{
|
||||
Account = libraryBook.Account,
|
||||
DateAdded = libraryBook.DateAdded,
|
||||
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title ?? "",
|
||||
Locale = libraryBook.Book.Locale,
|
||||
YearPublished = libraryBook.Book.DatePublished?.Year,
|
||||
DatePublished = libraryBook.Book.DatePublished,
|
||||
|
||||
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
|
||||
|
||||
|
||||
@@ -9,11 +9,15 @@ namespace FileManager
|
||||
/// <summary>Get valid filename. Advanced features incl. parameterized template</summary>
|
||||
public class FileNamingTemplate : NamingTemplate
|
||||
{
|
||||
public ReplacementCharacters ReplacementCharacters { get; }
|
||||
/// <param name="template">Proposed file name with optional html-styled template tags.</param>
|
||||
public FileNamingTemplate(string template) : base(template) { }
|
||||
public FileNamingTemplate(string template, ReplacementCharacters replacement) : base(template)
|
||||
{
|
||||
ReplacementCharacters = replacement ?? ReplacementCharacters.Default;
|
||||
}
|
||||
|
||||
/// <summary>Generate a valid path for this file or directory</summary>
|
||||
public LongPath GetFilePath(ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false)
|
||||
public LongPath GetFilePath(string fileExtension, bool returnFirstExisting = false)
|
||||
{
|
||||
string fileName =
|
||||
Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ?
|
||||
@@ -22,7 +26,7 @@ namespace FileManager
|
||||
|
||||
List<string> pathParts = new();
|
||||
|
||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, replacements));
|
||||
var paramReplacements = ParameterReplacements.ToDictionary(r => $"<{formatKey(r.Key)}>", r => formatValue(r.Value, ReplacementCharacters));
|
||||
|
||||
while (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
@@ -54,7 +58,7 @@ namespace FileManager
|
||||
return FileUtility
|
||||
.GetValidFilename(
|
||||
Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension,
|
||||
replacements,
|
||||
ReplacementCharacters,
|
||||
fileExtension,
|
||||
returnFirstExisting
|
||||
);
|
||||
|
||||
@@ -151,9 +151,9 @@ namespace FileManager
|
||||
/// <br/>- Perform <see cref="SaferMove"/>
|
||||
/// <br/>- Return valid path
|
||||
/// </summary>
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements)
|
||||
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements, string extension = null)
|
||||
{
|
||||
var extension = Path.GetExtension(source);
|
||||
extension = extension ?? Path.GetExtension(source);
|
||||
destination = GetValidFilename(destination, replacements, extension);
|
||||
SaferMove(source, destination);
|
||||
return destination;
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
var dataGrid = sender as DataGrid;
|
||||
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string>).Item1.Replace("\x200C", "").Replace("...", "");
|
||||
var item = (dataGrid.SelectedItem as Tuple<string, string, string>).Item3;
|
||||
if (string.IsNullOrWhiteSpace(item)) return;
|
||||
|
||||
var text = userEditTbox.Text;
|
||||
|
||||
userEditTbox.Text = text.Insert(Math.Min(Math.Max(0, userEditTbox.CaretIndex), text.Length), item);
|
||||
@@ -84,13 +86,14 @@ namespace LibationAvalonia.Dialogs
|
||||
Template = templates;
|
||||
Description = templates.Description;
|
||||
ListItems
|
||||
= new AvaloniaList<Tuple<string, string>>(
|
||||
= new AvaloniaList<Tuple<string, string, string>>(
|
||||
Template
|
||||
.GetTemplateTags()
|
||||
.Select(
|
||||
t => new Tuple<string, string>(
|
||||
t => new Tuple<string, string, string>(
|
||||
$"<{t.TagName.Replace("->", "-\x200C>").Replace("<-", "<\x200C-")}>",
|
||||
t.Description)
|
||||
t.Description,
|
||||
t.DefaultValue)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -108,13 +111,13 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
public string workingTemplateText => Template.Sanitize(UserTemplateText);
|
||||
public string workingTemplateText => Template.Sanitize(UserTemplateText, Configuration.Instance.ReplacementCharacters);
|
||||
private string _warningText;
|
||||
public string WarningText { get => _warningText; set => this.RaiseAndSetIfChanged(ref _warningText, value); }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public AvaloniaList<Tuple<string, string>> ListItems { get; set; }
|
||||
public AvaloniaList<Tuple<string, string, string>> ListItems { get; set; }
|
||||
|
||||
public void resetTextBox(string value) => UserTemplateText = value;
|
||||
|
||||
@@ -138,6 +141,8 @@ namespace LibationAvalonia.Dialogs
|
||||
var libraryBookDto = new LibraryBookDto
|
||||
{
|
||||
Account = "my account",
|
||||
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
|
||||
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
|
||||
@@ -526,15 +526,27 @@
|
||||
Margin="0,5,0,5"
|
||||
IsChecked="{Binding !AudioSettings.DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<StackPanel >
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
Margin="0,0,0,5"
|
||||
IsEnabled="{Binding !AudioSettings.DecryptToLossy}"
|
||||
IsChecked="{Binding AudioSettings.MoveMoovToBeginning, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding AudioSettings.MoveMoovToBeginningText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
Margin="0,5,0,5"
|
||||
IsEnabled="{Binding AudioSettings.IsMp3Supported}"
|
||||
IsChecked="{Binding AudioSettings.DecryptToLossy, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
@@ -548,7 +560,6 @@
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
IsVisible="{Binding AudioSettings.IsMp3Supported}"
|
||||
Grid.Column="1">
|
||||
|
||||
<controls:GroupBox
|
||||
|
||||
@@ -383,6 +383,7 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
|
||||
private bool _downloadClipsBookmarks;
|
||||
private bool _decryptToLossy;
|
||||
private bool _splitFilesByChapter;
|
||||
private bool _allowLibationFixup;
|
||||
private bool _lameTargetBitrate;
|
||||
@@ -391,8 +392,6 @@ namespace LibationAvalonia.Dialogs
|
||||
private int _lameVBRQuality;
|
||||
private string _chapterTitleTemplate;
|
||||
|
||||
public bool IsMp3Supported => Configuration.IsLinux || Configuration.IsWindows;
|
||||
|
||||
public AudioSettings(Configuration config)
|
||||
{
|
||||
LoadSettings(config);
|
||||
@@ -411,6 +410,7 @@ namespace LibationAvalonia.Dialogs
|
||||
StripUnabridged = config.StripUnabridged;
|
||||
ChapterTitleTemplate = config.ChapterTitleTemplate;
|
||||
DecryptToLossy = config.DecryptToLossy;
|
||||
MoveMoovToBeginning = config.MoveMoovToBeginning;
|
||||
LameTargetBitrate = config.LameTargetBitrate;
|
||||
LameDownsampleMono = config.LameDownsampleMono;
|
||||
LameConstantBitrate = config.LameConstantBitrate;
|
||||
@@ -433,6 +433,7 @@ namespace LibationAvalonia.Dialogs
|
||||
config.StripUnabridged = StripUnabridged;
|
||||
config.ChapterTitleTemplate = ChapterTitleTemplate;
|
||||
config.DecryptToLossy = DecryptToLossy;
|
||||
config.MoveMoovToBeginning = MoveMoovToBeginning;
|
||||
config.LameTargetBitrate = LameTargetBitrate;
|
||||
config.LameDownsampleMono = LameDownsampleMono;
|
||||
config.LameConstantBitrate = LameConstantBitrate;
|
||||
@@ -453,6 +454,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public string StripAudibleBrandingText { get; } = Configuration.GetDescription(nameof(Configuration.StripAudibleBrandAudio));
|
||||
public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
|
||||
public string ChapterTitleTemplateText { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate));
|
||||
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
||||
|
||||
public bool CreateCueSheet { get; set; }
|
||||
public bool DownloadCoverArt { get; set; }
|
||||
@@ -462,7 +464,8 @@ namespace LibationAvalonia.Dialogs
|
||||
public bool MergeOpeningAndEndCredits { get; set; }
|
||||
public bool StripAudibleBrandAudio { get; set; }
|
||||
public bool StripUnabridged { get; set; }
|
||||
public bool DecryptToLossy { get; set; }
|
||||
public bool DecryptToLossy { get => _decryptToLossy; set => this.RaiseAndSetIfChanged(ref _decryptToLossy, value); }
|
||||
public bool MoveMoovToBeginning { get; set; }
|
||||
|
||||
public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
|
||||
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
|
||||
|
||||
@@ -115,6 +115,9 @@ namespace LibationFileManager
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Move the mp4 moov atom to the beginning of the file?")]
|
||||
public bool MoveMoovToBeginning { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -25,10 +25,14 @@ namespace LibationFileManager
|
||||
public int BitRate { get; set; }
|
||||
public int SampleRate { get; set; }
|
||||
public int Channels { get; set; }
|
||||
}
|
||||
public DateTime FileDate { get; set; } = DateTime.Now;
|
||||
public DateTime? DatePublished { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class LibraryBookDto : BookDto
|
||||
{
|
||||
public string Account { get; set; }
|
||||
{
|
||||
public DateTime? DateAdded { get; set; }
|
||||
public string Account { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,19 @@ namespace LibationFileManager
|
||||
{
|
||||
public sealed class TemplateTags : Enumeration<TemplateTags>
|
||||
{
|
||||
public string TagName => DisplayName;
|
||||
public string TagName => DisplayName;
|
||||
public string DefaultValue { get; }
|
||||
public string Description { get; }
|
||||
public bool IsChapterOnly { get; }
|
||||
|
||||
private static int value = 0;
|
||||
private TemplateTags(string tagName, string description, bool isChapterOnly = false) : base(value++, tagName)
|
||||
private TemplateTags(string tagName, string description, bool isChapterOnly = false, string defaultValue = null) : base(value++, tagName)
|
||||
{
|
||||
Description = description;
|
||||
IsChapterOnly = isChapterOnly;
|
||||
}
|
||||
DefaultValue = defaultValue ?? $"<{tagName}>";
|
||||
|
||||
}
|
||||
|
||||
// putting these first is the incredibly lazy way to make them show up first in the EditTemplateDialog
|
||||
public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true);
|
||||
@@ -41,8 +44,11 @@ namespace LibationFileManager
|
||||
public static TemplateTags Locale { get; } = new TemplateTags("locale", "Region/country");
|
||||
public static TemplateTags YearPublished { get; } = new TemplateTags("year", "Year published");
|
||||
|
||||
// Special case. Isn't mapped to a replacement in Templates.cs
|
||||
// Special cases. Aren't mapped to replacements in Templates.cs
|
||||
// Included here for display by EditTemplateDialog
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series");
|
||||
public static TemplateTags FileDate { get; } = new TemplateTags("file date [...]", "File date/time. e.g. yyyy-MM-dd HH-mm", false, $"<file date [{Templates.DEFAULT_DATE_FORMAT}]>");
|
||||
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date [...]", "Publication date. e.g. yyyy-MM-dd", false, $"<pub date [{Templates.DEFAULT_DATE_FORMAT}]>");
|
||||
public static TemplateTags DateAdded { get; } = new TemplateTags("date added [...]", "Date added to your Audible account. e.g. yyyy-MM-dd", false, $"<date added [{Templates.DEFAULT_DATE_FORMAT}]>");
|
||||
public static TemplateTags IfSeries { get; } = new TemplateTags("if series->...<-if series", "Only include if part of a series", false, "<if series-><-if series>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
@@ -102,16 +103,21 @@ namespace LibationFileManager
|
||||
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, string fileExtension)
|
||||
=> string.IsNullOrWhiteSpace(template)
|
||||
? ""
|
||||
: getFileNamingTemplate(libraryBookDto, template, null, fileExtension)
|
||||
.GetFilePath(Configuration.Instance.ReplacementCharacters, fileExtension).PathWithoutPrefix;
|
||||
: getFileNamingTemplate(libraryBookDto, template, null, fileExtension, Configuration.Instance.ReplacementCharacters)
|
||||
.GetFilePath(fileExtension).PathWithoutPrefix;
|
||||
|
||||
public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static Regex fileDateTagRegex { get; } = new Regex(@"<file\s*?date\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static Regex dateAddedTagRegex { get; } = new Regex(@"<date\s*?added\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static Regex datePublishedTagRegex { get; } = new Regex(@"<pub\s*?date\s*?(?:\[([^\[\]]*?)\]\s*?)?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension)
|
||||
internal static FileNamingTemplate getFileNamingTemplate(LibraryBookDto libraryBookDto, string template, string dirFullPath, string extension, ReplacementCharacters replacements)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
|
||||
ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto));
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
dirFullPath = dirFullPath?.Trim() ?? "";
|
||||
|
||||
// for non-series, remove <if series-> and <-if series> tags and everything in between
|
||||
@@ -120,10 +126,16 @@ namespace LibationFileManager
|
||||
template,
|
||||
string.IsNullOrWhiteSpace(libraryBookDto.SeriesName) ? "" : "$1");
|
||||
|
||||
//Get date replacement parameters. Sanitizes the format text and replaces
|
||||
//the template with the sanitized text before creating FileNamingTemplate
|
||||
var fileDateParams = getSanitizeDateReplacementParameters(fileDateTagRegex, ref template, replacements, libraryBookDto.FileDate);
|
||||
var dateAddedParams = getSanitizeDateReplacementParameters(dateAddedTagRegex, ref template, replacements, libraryBookDto.DateAdded);
|
||||
var pubDateParams = getSanitizeDateReplacementParameters(datePublishedTagRegex, ref template, replacements, libraryBookDto.DatePublished);
|
||||
|
||||
var t = template + FileUtility.GetStandardizedExtension(extension);
|
||||
var fullfilename = dirFullPath == "" ? t : Path.Combine(dirFullPath, t);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename);
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename, replacements);
|
||||
|
||||
var title = libraryBookDto.Title ?? "";
|
||||
var titleShort = title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':'));
|
||||
@@ -144,7 +156,73 @@ namespace LibationFileManager
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.Locale, libraryBookDto.Locale);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.YearPublished, libraryBookDto.YearPublished?.ToString() ?? "1900");
|
||||
|
||||
return fileNamingTemplate;
|
||||
//Add the sanitized replacement parameters
|
||||
foreach (var param in fileDateParams)
|
||||
fileNamingTemplate.ParameterReplacements.AddIfNotContains(param);
|
||||
foreach (var param in dateAddedParams)
|
||||
fileNamingTemplate.ParameterReplacements.AddIfNotContains(param);
|
||||
foreach (var param in pubDateParams)
|
||||
fileNamingTemplate.ParameterReplacements.AddIfNotContains(param);
|
||||
|
||||
return fileNamingTemplate;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DateTime Tags
|
||||
|
||||
/// <param name="template">the file naming template. Any found date tags will be sanitized,
|
||||
/// and the template's original date tag will be replaced with the sanitized tag.</param>
|
||||
/// <returns>A list of parameter replacement key-value pairs</returns>
|
||||
private static List<KeyValuePair<string, object>> getSanitizeDateReplacementParameters(Regex datePattern, ref string template, ReplacementCharacters replacements, DateTime? dateTime)
|
||||
{
|
||||
List<KeyValuePair<string, object>> dateParams = new();
|
||||
|
||||
foreach (Match dateTag in datePattern.Matches(template))
|
||||
{
|
||||
var sanitizedTag = sanitizeDateParameterTag(dateTag, replacements, out var sanitizedFormatter);
|
||||
if (tryFormatDateTime(dateTime, sanitizedFormatter, replacements, out var formattedDateString))
|
||||
{
|
||||
dateParams.Add(new(sanitizedTag, formattedDateString));
|
||||
template = template.Replace(dateTag.Value, sanitizedTag);
|
||||
}
|
||||
}
|
||||
return dateParams;
|
||||
}
|
||||
|
||||
/// <returns>a date parameter replacement tag with the format string sanitized</returns>
|
||||
private static string sanitizeDateParameterTag(Match dateTag, ReplacementCharacters replacements, out string sanitizedFormatter)
|
||||
{
|
||||
if (dateTag.Groups.Count != 2 || string.IsNullOrWhiteSpace(dateTag.Groups[1].Value))
|
||||
{
|
||||
sanitizedFormatter = DEFAULT_DATE_FORMAT;
|
||||
return dateTag.Value;
|
||||
}
|
||||
|
||||
var formatter = dateTag.Groups[1].Value;
|
||||
|
||||
sanitizedFormatter = replacements.ReplaceFilenameChars(formatter).Trim();
|
||||
|
||||
return dateTag.Value.Replace(formatter, sanitizedFormatter);
|
||||
}
|
||||
|
||||
private static bool tryFormatDateTime(DateTime? dateTime, string sanitizedFormatter, ReplacementCharacters replacements, out string formattedDateString)
|
||||
{
|
||||
if (!dateTime.HasValue)
|
||||
{
|
||||
formattedDateString = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
formattedDateString = replacements.ReplaceFilenameChars(dateTime.Value.ToString(sanitizedFormatter)).Trim();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
formattedDateString = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -153,10 +231,17 @@ namespace LibationFileManager
|
||||
// yeah, this line is a little funky but it works when you think through it. also: trust the unit tests
|
||||
.Where(t => IsChapterized || !t.IsChapterOnly);
|
||||
|
||||
public string Sanitize(string template)
|
||||
public string Sanitize(string template, ReplacementCharacters replacements)
|
||||
{
|
||||
var value = template ?? "";
|
||||
|
||||
// Replace invalid filename characters in the DateTime format provider so we don't trip any alarms.
|
||||
// Illegal filename characters in the formatter are allowed because they will be replaced by
|
||||
// getFileNamingTemplate()
|
||||
value = fileDateTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _));
|
||||
value = dateAddedTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _));
|
||||
value = datePublishedTagRegex.Replace(value, m => sanitizeDateParameterTag(m, replacements, out _));
|
||||
|
||||
// don't use alt slash
|
||||
value = value.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||
|
||||
@@ -203,7 +288,7 @@ namespace LibationFileManager
|
||||
|
||||
// must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
|
||||
if (ReplacementCharacters.ContainsInvalidPathChar(template.Replace("<", "").Replace(">", "")))
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
return new[] { ERROR_INVALID_FILE_NAME_CHAR };
|
||||
|
||||
return Valid;
|
||||
}
|
||||
@@ -214,8 +299,8 @@ namespace LibationFileManager
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null)
|
||||
.GetFilePath(Configuration.Instance.ReplacementCharacters, string.Empty);
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null, Configuration.Instance.ReplacementCharacters)
|
||||
.GetFilePath(string.Empty);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -237,8 +322,8 @@ namespace LibationFileManager
|
||||
#region to file name
|
||||
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
|
||||
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
|
||||
.GetFilePath(Configuration.Instance.ReplacementCharacters, extension, returnFirstExisting);
|
||||
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension, Configuration.Instance.ReplacementCharacters)
|
||||
.GetFilePath(extension, returnFirstExisting);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -279,14 +364,21 @@ namespace LibationFileManager
|
||||
|
||||
replacements ??= Configuration.Instance.ReplacementCharacters;
|
||||
var fileExtension = Path.GetExtension(props.OutputFileName);
|
||||
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, fileExtension);
|
||||
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, fileExtension, replacements);
|
||||
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
|
||||
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
|
||||
|
||||
return fileNamingTemplate.GetFilePath(replacements, fileExtension).PathWithoutPrefix;
|
||||
foreach (Match dateTag in fileDateTagRegex.Matches(fileNamingTemplate.Template))
|
||||
{
|
||||
var sanitizedTag = sanitizeDateParameterTag(dateTag, replacements, out string sanitizedFormatter);
|
||||
if (tryFormatDateTime(props.FileDate, sanitizedFormatter, replacements, out var formattedDateString))
|
||||
fileNamingTemplate.ParameterReplacements[sanitizedTag] = formattedDateString;
|
||||
}
|
||||
|
||||
return fileNamingTemplate.GetFilePath(fileExtension).PathWithoutPrefix;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -9,5 +9,8 @@ namespace LibationFileManager
|
||||
{
|
||||
public static void AddParameterReplacement(this NamingTemplate fileNamingTemplate, TemplateTags templateTags, object value)
|
||||
=> fileNamingTemplate.AddParameterReplacement(templateTags.TagName, value);
|
||||
|
||||
public static void AddUniqueParameterReplacement(this NamingTemplate namingTemplate, string key, object value)
|
||||
=> namingTemplate.ParameterReplacements[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace LibationWinForms.Dialogs
|
||||
private string workingTemplateText
|
||||
{
|
||||
get => _workingTemplateText;
|
||||
set => _workingTemplateText = template.Sanitize(value);
|
||||
set => _workingTemplateText = template.Sanitize(value, Configuration.Instance.ReplacementCharacters);
|
||||
}
|
||||
|
||||
private void resetTextBox(string value) => this.templateTb.Text = workingTemplateText = value;
|
||||
@@ -59,7 +59,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
// populate list view
|
||||
foreach (var tag in template.GetTemplateTags())
|
||||
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }));
|
||||
listView1.Items.Add(new ListViewItem(new[] { $"<{tag.TagName}>", tag.Description }) { Tag = tag.DefaultValue });
|
||||
}
|
||||
|
||||
private void resetToDefaultBtn_Click(object sender, EventArgs e) => resetTextBox(template.DefaultTemplate);
|
||||
@@ -73,6 +73,8 @@ namespace LibationWinForms.Dialogs
|
||||
var libraryBookDto = new LibraryBookDto
|
||||
{
|
||||
Account = "my account",
|
||||
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
|
||||
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
@@ -207,9 +209,11 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void listView1_DoubleClick(object sender, EventArgs e)
|
||||
{
|
||||
var itemText = listView1.SelectedItems[0].Text.Replace("...", "");
|
||||
var text = templateTb.Text;
|
||||
var itemText = listView1.SelectedItems[0].Tag as string;
|
||||
|
||||
if (string.IsNullOrEmpty(itemText)) return;
|
||||
|
||||
var text = templateTb.Text;
|
||||
var selStart = Math.Min(Math.Max(0, templateTb.SelectionStart), text.Length);
|
||||
|
||||
templateTb.Text = text.Insert(selStart, itemText);
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace LibationWinForms.Dialogs
|
||||
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
|
||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
||||
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
|
||||
|
||||
clipsBookmarksFormatCb.Items.AddRange(
|
||||
new object[]
|
||||
@@ -37,6 +38,7 @@ namespace LibationWinForms.Dialogs
|
||||
stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio;
|
||||
convertLosslessRb.Checked = !config.DecryptToLossy;
|
||||
convertLossyRb.Checked = config.DecryptToLossy;
|
||||
moveMoovAtomCbox.Checked = config.MoveMoovToBeginning;
|
||||
|
||||
lameTargetBitrateRb.Checked = config.LameTargetBitrate;
|
||||
lameTargetQualityRb.Checked = !config.LameTargetBitrate;
|
||||
@@ -70,6 +72,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
||||
config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked;
|
||||
config.DecryptToLossy = convertLossyRb.Checked;
|
||||
config.MoveMoovToBeginning = moveMoovAtomCbox.Checked;
|
||||
|
||||
config.LameTargetBitrate = lameTargetBitrateRb.Checked;
|
||||
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
|
||||
@@ -107,6 +110,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
moveMoovAtomCbox.Enabled = convertLosslessRb.Checked;
|
||||
lameTargetRb_CheckedChanged(sender, e);
|
||||
LameMatchSourceBRCbox_CheckedChanged(sender, e);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
this.clipsBookmarksFormatCb = new System.Windows.Forms.ComboBox();
|
||||
this.downloadClipsBookmarksCbox = new System.Windows.Forms.CheckBox();
|
||||
this.audiobookFixupsGb = new System.Windows.Forms.GroupBox();
|
||||
this.moveMoovAtomCbox = new System.Windows.Forms.CheckBox();
|
||||
this.stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
|
||||
this.chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
|
||||
this.chapterTitleTemplateBtn = new System.Windows.Forms.Button();
|
||||
@@ -294,7 +295,7 @@
|
||||
// convertLossyRb
|
||||
//
|
||||
this.convertLossyRb.AutoSize = true;
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(13, 136);
|
||||
this.convertLossyRb.Location = new System.Drawing.Point(13, 158);
|
||||
this.convertLossyRb.Name = "convertLossyRb";
|
||||
this.convertLossyRb.Size = new System.Drawing.Size(329, 19);
|
||||
this.convertLossyRb.TabIndex = 12;
|
||||
@@ -675,6 +676,7 @@
|
||||
//
|
||||
// audiobookFixupsGb
|
||||
//
|
||||
this.audiobookFixupsGb.Controls.Add(this.moveMoovAtomCbox);
|
||||
this.audiobookFixupsGb.Controls.Add(this.splitFilesByChapterCbox);
|
||||
this.audiobookFixupsGb.Controls.Add(this.stripUnabridgedCbox);
|
||||
this.audiobookFixupsGb.Controls.Add(this.convertLosslessRb);
|
||||
@@ -682,11 +684,21 @@
|
||||
this.audiobookFixupsGb.Controls.Add(this.stripAudibleBrandingCbox);
|
||||
this.audiobookFixupsGb.Location = new System.Drawing.Point(6, 169);
|
||||
this.audiobookFixupsGb.Name = "audiobookFixupsGb";
|
||||
this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 160);
|
||||
this.audiobookFixupsGb.Size = new System.Drawing.Size(403, 185);
|
||||
this.audiobookFixupsGb.TabIndex = 19;
|
||||
this.audiobookFixupsGb.TabStop = false;
|
||||
this.audiobookFixupsGb.Text = "Audiobook Fix-ups";
|
||||
//
|
||||
// moveMoovAtomCbox
|
||||
//
|
||||
this.moveMoovAtomCbox.AutoSize = true;
|
||||
this.moveMoovAtomCbox.Location = new System.Drawing.Point(23, 133);
|
||||
this.moveMoovAtomCbox.Name = "moveMoovAtomCbox";
|
||||
this.moveMoovAtomCbox.Size = new System.Drawing.Size(188, 19);
|
||||
this.moveMoovAtomCbox.TabIndex = 14;
|
||||
this.moveMoovAtomCbox.Text = "[MoveMoovToBeginning desc]";
|
||||
this.moveMoovAtomCbox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// stripUnabridgedCbox
|
||||
//
|
||||
this.stripUnabridgedCbox.AutoSize = true;
|
||||
@@ -701,7 +713,7 @@
|
||||
//
|
||||
this.chapterTitleTemplateGb.Controls.Add(this.chapterTitleTemplateBtn);
|
||||
this.chapterTitleTemplateGb.Controls.Add(this.chapterTitleTemplateTb);
|
||||
this.chapterTitleTemplateGb.Location = new System.Drawing.Point(6, 335);
|
||||
this.chapterTitleTemplateGb.Location = new System.Drawing.Point(6, 360);
|
||||
this.chapterTitleTemplateGb.Name = "chapterTitleTemplateGb";
|
||||
this.chapterTitleTemplateGb.Size = new System.Drawing.Size(842, 54);
|
||||
this.chapterTitleTemplateGb.TabIndex = 18;
|
||||
@@ -738,7 +750,7 @@
|
||||
this.lameOptionsGb.Controls.Add(this.groupBox2);
|
||||
this.lameOptionsGb.Location = new System.Drawing.Point(415, 6);
|
||||
this.lameOptionsGb.Name = "lameOptionsGb";
|
||||
this.lameOptionsGb.Size = new System.Drawing.Size(433, 323);
|
||||
this.lameOptionsGb.Size = new System.Drawing.Size(433, 348);
|
||||
this.lameOptionsGb.TabIndex = 14;
|
||||
this.lameOptionsGb.TabStop = false;
|
||||
this.lameOptionsGb.Text = "Mp3 Encoding Options";
|
||||
@@ -1240,5 +1252,6 @@
|
||||
private System.Windows.Forms.CheckBox useCoverAsFolderIconCb;
|
||||
private System.Windows.Forms.ComboBox clipsBookmarksFormatCb;
|
||||
private System.Windows.Forms.CheckBox downloadClipsBookmarksCbox;
|
||||
private System.Windows.Forms.CheckBox moveMoovAtomCbox;
|
||||
}
|
||||
}
|
||||
@@ -36,10 +36,10 @@ namespace FileNamingTemplateTests
|
||||
extension = FileUtility.GetStandardizedExtension(extension);
|
||||
var fullfilename = Path.Combine(dirFullPath, template + extension);
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename);
|
||||
var fileNamingTemplate = new FileNamingTemplate(fullfilename, Replacements);
|
||||
fileNamingTemplate.AddParameterReplacement("title", filename);
|
||||
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
|
||||
return fileNamingTemplate.GetFilePath(Replacements, extension).PathWithoutPrefix;
|
||||
return fileNamingTemplate.GetFilePath(extension).PathWithoutPrefix;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -61,10 +61,10 @@ namespace FileNamingTemplateTests
|
||||
var estension = Path.GetExtension(originalPath);
|
||||
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + estension;
|
||||
|
||||
var fileNamingTemplate = new FileNamingTemplate(t);
|
||||
var fileNamingTemplate = new FileNamingTemplate(t, Replacements);
|
||||
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
|
||||
fileNamingTemplate.AddParameterReplacement("title", suffix);
|
||||
return fileNamingTemplate.GetFilePath(Replacements, estension).PathWithoutPrefix;
|
||||
return fileNamingTemplate.GetFilePath(estension).PathWithoutPrefix;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -74,9 +74,9 @@ namespace FileNamingTemplateTests
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformID)
|
||||
{
|
||||
var fileNamingTemplate = new FileNamingTemplate(inStr);
|
||||
var fileNamingTemplate = new FileNamingTemplate(inStr, Replacements);
|
||||
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
|
||||
fileNamingTemplate.GetFilePath(Replacements, "txt").PathWithoutPrefix.Should().Be(outStr);
|
||||
fileNamingTemplate.GetFilePath("txt").PathWithoutPrefix.Should().Be(outStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -26,11 +26,32 @@ namespace TemplatesTests
|
||||
=> new()
|
||||
{
|
||||
Account = "my account",
|
||||
DateAdded = new DateTime(2022, 6, 9, 0, 0, 0),
|
||||
DatePublished = new DateTime(2017, 2, 27, 0, 0, 0),
|
||||
FileDate = new DateTime(2023, 1, 28, 0, 0, 0),
|
||||
AudibleProductId = "asin",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
YearPublished = 2017,
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
YearPublished = 2017,
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = seriesName ?? "",
|
||||
SeriesNumber = "1",
|
||||
BitRate = 128,
|
||||
SampleRate = 44100,
|
||||
Channels = 2
|
||||
};
|
||||
|
||||
public static LibraryBookDto GetLibraryBookWithNullDates(string seriesName = "Sherlock Holmes")
|
||||
=> new()
|
||||
{
|
||||
Account = "my account",
|
||||
FileDate = new DateTime(2023, 1, 28, 0, 0, 0),
|
||||
AudibleProductId = "asin",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
YearPublished = 2017,
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = seriesName ?? "",
|
||||
SeriesNumber = "1",
|
||||
@@ -71,47 +92,156 @@ namespace TemplatesTests
|
||||
[DataRow(null, @"C:\", "ext")]
|
||||
[ExpectedException(typeof(ArgumentNullException))]
|
||||
public void arg_null_exception(string template, string dirFullPath, string extension)
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension);
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("", @"C:\foo\bar", "ext")]
|
||||
[DataRow(" ", @"C:\foo\bar", "ext")]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void arg_exception(string template, string dirFullPath, string extension)
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension);
|
||||
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("f.txt", @"C:\foo\bar", "", @"C:\foo\bar\f.txt", PlatformID.Win32NT)]
|
||||
[DataRow("f.txt", @"/foo/bar", "", @"/foo/bar/f.txt", PlatformID.Unix)]
|
||||
[DataRow("f.txt", @"C:\foo\bar", ".ext", @"C:\foo\bar\f.txt.ext", PlatformID.Win32NT)]
|
||||
[DataRow("f.txt", @"/foo/bar", ".ext", @"/foo/bar/f.txt.ext", PlatformID.Unix)]
|
||||
[DataRow("f", @"C:\foo\bar", ".ext", @"C:\foo\bar\f.ext", PlatformID.Win32NT)]
|
||||
[DataRow("f", @"/foo/bar", ".ext", @"/foo/bar/f.ext", PlatformID.Unix)]
|
||||
[DataRow("<id>", @"C:\foo\bar", ".ext", @"C:\foo\bar\asin.ext", PlatformID.Win32NT)]
|
||||
[DataRow("<id>", @"/foo/bar", ".ext", @"/foo/bar/asin.ext", PlatformID.Unix)]
|
||||
[DataRow("<bitrate> - <samplerate> - <channels>", @"C:\foo\bar", ".ext", @"C:\foo\bar\128 - 44100 - 2.ext", PlatformID.Win32NT)]
|
||||
[DataRow("<bitrate> - <samplerate> - <channels>", @"/foo/bar", ".ext", @"/foo/bar/128 - 44100 - 2.ext", PlatformID.Unix)]
|
||||
[DataRow("<year> - <channels>", @"C:\foo\bar", ".ext", @"C:\foo\bar\2017 - 2.ext", PlatformID.Win32NT)]
|
||||
[DataRow("<year> - <channels>", @"/foo/bar", ".ext", @"/foo/bar/2017 - 2.ext", PlatformID.Unix)]
|
||||
[DataRow("(000.0) <year> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\(000.0) 2017 - 2.ext", PlatformID.Win32NT)]
|
||||
[DataRow("(000.0) <year> - <channels>", @"/foo/bar", ".ext", @"/foo/bar/(000.0) 2017 - 2.ext", PlatformID.Unix)]
|
||||
public void Tests(string template, string dirFullPath, string extension, string expected, PlatformID platformID)
|
||||
[DataRow("f.txt", @"C:\foo\bar", "", @"C:\foo\bar\f.txt")]
|
||||
[DataRow("f.txt", @"C:\foo\bar", ".ext", @"C:\foo\bar\f.txt.ext")]
|
||||
[DataRow("f", @"C:\foo\bar", ".ext", @"C:\foo\bar\f.ext")]
|
||||
[DataRow("<id>", @"C:\foo\bar", ".ext", @"C:\foo\bar\asin.ext")]
|
||||
[DataRow("<bitrate> - <samplerate> - <channels>", @"C:\foo\bar", ".ext", @"C:\foo\bar\128 - 44100 - 2.ext")]
|
||||
[DataRow("<year> - <channels>", @"C:\foo\bar", ".ext", @"C:\foo\bar\2017 - 2.ext")]
|
||||
[DataRow("(000.0) <year> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\(000.0) 2017 - 2.ext")]
|
||||
public void Tests(string template, string dirFullPath, string extension, string expected)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformID)
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension)
|
||||
.GetFilePath(Replacements, extension)
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
||||
{
|
||||
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||
}
|
||||
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
||||
.GetFilePath(extension)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<id> - <filedate[yy-MM-dd]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")]
|
||||
[DataRow("<id> - <filedate [ yy-MM-dd ] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")]
|
||||
[DataRow("<id> - <file date [yy-MM-dd] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")]
|
||||
[DataRow("<id> - <file date[yy-MM-dd]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 23-01-28.m4b")]
|
||||
[DataRow("<id> - <file date[]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
[DataRow("<id> - <filedate[]>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
[DataRow("<id> - <filedate [ ] >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
[DataRow("<id> - <filedate>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
[DataRow("<id> - <filedate >", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
[DataRow("<id> - <file date>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
[DataRow("<id> - <file date>", @"C:\foo\bar", "m4b", @"C:\foo\bar\asin - 2023-01-28.m4b")]
|
||||
public void DateFormat_pattern(string template, string dirFullPath, string extension, string expected)
|
||||
{
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
||||
{
|
||||
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||
}
|
||||
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
||||
.GetFilePath(extension)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<filedate[h]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[h]>.m4b")]
|
||||
[DataRow("< filedate[yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\< filedate[yyyy]>.m4b")]
|
||||
[DataRow("<filedate[yyyy][]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[yyyy][]>.m4b")]
|
||||
[DataRow("<filedate[[yyyy]]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[[yyyy]]>.m4b")]
|
||||
[DataRow("<filedate[yyyy[]]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[yyyy[]]>.m4b")]
|
||||
[DataRow("<filedate yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate yyyy]>.m4b")]
|
||||
[DataRow("<filedate ]yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate ]yyyy]>.m4b")]
|
||||
[DataRow("<filedate [yyyy>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate [yyyy>.m4b")]
|
||||
[DataRow("<filedate [yyyy[>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate [yyyy[>.m4b")]
|
||||
[DataRow("<filedate yyyy>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate yyyy>.m4b")]
|
||||
[DataRow("<filedate[yyyy]", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filedate[yyyy].m4b")]
|
||||
[DataRow("<fil edate[yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<fil edate[yyyy]>.m4b")]
|
||||
[DataRow("<filed ate[yyyy]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\<filed ate[yyyy]>.m4b")]
|
||||
public void DateFormat_invalid(string template, string dirFullPath, string extension, string expected)
|
||||
{
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
||||
{
|
||||
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
||||
expected = expected.Replace("C:", "").Replace('\\', '/').Replace('<', '<').Replace('>','>');
|
||||
}
|
||||
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
||||
.GetFilePath(extension)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28 22-06-09 17-02.m4b")]
|
||||
[DataRow("<filedate[yy-MM-dd]> <filedate[yy-MM-dd]> <filedate[yy-MM-dd]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28 23-01-28 23-01-28.m4b")]
|
||||
[DataRow("<file date [ yy-MM-dd ] > <filedate [ yy-MM-dd ] > <file date [ yy-MM-dd] >", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28 23-01-28 23-01-28.m4b")]
|
||||
public void DateFormat_multiple(string template, string dirFullPath, string extension, string expected)
|
||||
{
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
||||
{
|
||||
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||
}
|
||||
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
||||
.GetFilePath(extension)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<id> - <pubdate[MM/dd/yy HH:mm]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\asin - 02∕27∕17 00꞉00.m4b", PlatformID.Win32NT)]
|
||||
[DataRow("<id> - <pubdate[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 02∕27∕17 00:00.m4b", PlatformID.Unix)]
|
||||
[DataRow("<id> - <filedate[MM/dd/yy HH:mm]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\asin - 01∕28∕23 00꞉00.m4b", PlatformID.Win32NT)]
|
||||
[DataRow("<id> - <filedate[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 01∕28∕23 00:00.m4b", PlatformID.Unix)]
|
||||
[DataRow("<id> - <date added[MM/dd/yy HH:mm]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\asin - 06∕09∕22 00꞉00.m4b", PlatformID.Win32NT)]
|
||||
[DataRow("<id> - <date added[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 06∕09∕22 00:00.m4b", PlatformID.Unix)]
|
||||
public void DateFormat_illegal(string template, string dirFullPath, string extension, string expected, PlatformID platformID)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformID)
|
||||
{
|
||||
Templates.File.HasWarnings(template).Should().BeTrue();
|
||||
Templates.File.HasWarnings(Templates.File.Sanitize(template, Replacements)).Should().BeFalse();
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension, Replacements)
|
||||
.GetFilePath(extension)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<filedate[yy-MM-dd]> <date added[yy-MM-dd]> <pubdate[yy-MM]>", @"C:\foo\bar", ".m4b", @"C:\foo\bar\23-01-28.m4b")]
|
||||
public void DateFormat_null(string template, string dirFullPath, string extension, string expected)
|
||||
{
|
||||
if (Environment.OSVersion.Platform is not PlatformID.Win32NT)
|
||||
{
|
||||
dirFullPath = dirFullPath.Replace("C:", "").Replace('\\', '/');
|
||||
expected = expected.Replace("C:", "").Replace('\\', '/');
|
||||
}
|
||||
|
||||
Templates.getFileNamingTemplate(GetLibraryBookWithNullDates(), template, dirFullPath, extension, Replacements)
|
||||
.GetFilePath(extension)
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\a\b", @"C:\a\b\foobar.ext", PlatformID.Win32NT)]
|
||||
[DataRow(@"/a/b", @"/a/b/foobar.ext", PlatformID.Unix)]
|
||||
public void IfSeries_empty(string directory, string expected, PlatformID platformID)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformID)
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext")
|
||||
.GetFilePath(Replacements, ".ext")
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext", Replacements)
|
||||
.GetFilePath(".ext")
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -122,8 +252,8 @@ namespace TemplatesTests
|
||||
public void IfSeries_no_series(string directory, string expected, PlatformID platformID)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformID)
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext")
|
||||
.GetFilePath(Replacements, ".ext")
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext", Replacements)
|
||||
.GetFilePath(".ext")
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
@@ -134,8 +264,8 @@ namespace TemplatesTests
|
||||
public void IfSeries_with_series(string directory, string expected, PlatformID platformID)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == platformID)
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext")
|
||||
.GetFilePath(Replacements, ".ext")
|
||||
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext", Replacements)
|
||||
.GetFilePath(".ext")
|
||||
.PathWithoutPrefix
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user