Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6ffb726
Improve media detection, TMDB fallback, and CLI key handling
mozartsempiano Mar 17, 2026
ba64025
Support absolute TV episode filenames and more video formats
mozartsempiano Mar 18, 2026
4543360
ci: add build workflow
jamerst Mar 27, 2026
e589227
ci: update action versions
jamerst Mar 27, 2026
a562828
ci: add publish action
jamerst Mar 27, 2026
b9941c0
fix: lock file
jamerst Mar 27, 2026
8ef64a9
ci: set environment for publish job
jamerst Mar 27, 2026
60a7076
fix: set RuntimeIdentifiers for CLI project
jamerst Mar 27, 2026
c0b990b
Update README
mozartsempiano Apr 12, 2026
cbce382
Add --include-adult tag
mozartsempiano Apr 12, 2026
23d8143
Improve subtitle renaming and moving
mozartsempiano Apr 12, 2026
f2d846f
IImprove poster tagging
mozartsempiano Apr 12, 2026
e969314
ci: upload artifacts to release
jamerst Apr 16, 2026
0b44cbb
fix: lock file
jamerst Apr 16, 2026
9f78c8c
fix: set GH_TOKEN variable in upload step
jamerst Apr 16, 2026
b9f42db
ci: remove explicit restore step to fix trimming
jamerst Apr 16, 2026
decd4af
fix: restore build workflow restore step
jamerst Apr 16, 2026
f1e3aa8
fix: restore publish workflow restore step and set PublishTrimmed at …
jamerst Apr 16, 2026
2fc27c1
ci: disable lockfile (doesn't work well with multiple runtimes and tr…
jamerst Apr 16, 2026
7eba124
Merge pull request #34 from mozartsempiano/master
jamerst May 9, 2026
80657dc
Merge branch 'github-actions' into dev
jamerst May 9, 2026
fdf6aac
refactor: return enum from ProcessAsync to give more detail on reason…
jamerst May 9, 2026
36f8cad
refactor: avoid extra API call for movies where possible
jamerst May 9, 2026
1579d83
feat: add absolute path rename pattern support
jamerst May 18, 2026
f07b4bf
fix warning
jamerst May 18, 2026
f13cfee
test: add CLI integration tests
jamerst May 24, 2026
c316463
fix: set TMDB_API_KEY for integration tests
jamerst May 24, 2026
71f3ce0
fix: file name replacement config deserialisation
jamerst May 27, 2026
b5c0d8f
test: add integration test for processing files
jamerst May 29, 2026
57f6598
debug: print stdout for failing test
jamerst May 30, 2026
1c945d7
test: strip ANSI colour codes from stdout
jamerst May 30, 2026
ba51bcf
ci: disable fail-fast on integration tests
jamerst May 30, 2026
4f2d276
test: dispose FileStream when creating test file
jamerst May 30, 2026
b473f0b
fix: IsAlreadyNamedCorrectly not handling relative paths
jamerst Jun 4, 2026
99c99e3
test: add integration test for absolute rename pattern
jamerst Jun 4, 2026
829c1a1
build: restore key check for releases only
jamerst Jun 4, 2026
90e3252
chore: update packages
jamerst Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: publish
on:
release:
types:
- created

jobs:
publish:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-runtime: [ 'linux-x64', 'osx-arm64', 'osx-x64', 'win-x64' ]

steps:
- uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Restore dependencies
# need to set /p:PublishTrimmed here as well because --no-restore on the publish step causes trimming to silently fail
# https://github.com/dotnet/sdk/issues/37049
run: dotnet restore -p:PublishTrimmed=true

- name: Publish
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
run: dotnet publish AutoTag.CLI -r ${{ matrix.dotnet-runtime }} -c Release --no-restore

- name: Upload
env:
GH_TOKEN: ${{ github.token }}
run: |
outputName="autotag-${{ github.ref_name }}_${{ matrix.dotnet-runtime }}.zip"
finalOutputName="${outputName/osx/macos}"
zip -j $finalOutputName AutoTag.CLI/bin/Release/net*/${{ matrix.dotnet-runtime }}/publish/autotag*
gh release upload ${{ github.ref_name }} $finalOutputName
51 changes: 51 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: test

on: [ push, pull_request ]

jobs:
unit-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Test
run: dotnet test AutoTag.Core.Test --no-build --verbosity normal

integration-tests:
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ "ubuntu-latest", "macos-latest", "windows-latest" ]

steps:
- uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Test
env:
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
run: dotnet test AutoTag.CLI.Test --no-build --verbosity normal
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,5 @@ FodyWeavers.xsd

# Ignore JetBrains config files
.idea/

.env
31 changes: 31 additions & 0 deletions AutoTag.CLI.Test/AutoTag.CLI.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.4.0"/>
<PackageReference Include="CliWrap" Version="3.10.2"/>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0"/>
<PackageReference Include="Spectre.Console.Cli.Testing" Version="0.55.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
<PackageReference Include="xunit.v3" Version="3.2.2"/>
</ItemGroup>

<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AutoTag.CLI\AutoTag.CLI.csproj"/>
<ProjectReference Include="..\AutoTag.Core\AutoTag.Core.csproj"/>
</ItemGroup>
</Project>
97 changes: 97 additions & 0 deletions AutoTag.CLI.Test/ConfigTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Text.Json;
using AutoTag.CLI.Test.Helpers;
using AutoTag.Core.Config;
using AutoTag.Core.Files;
using AwesomeAssertions.Execution;

namespace AutoTag.CLI.Test;

public class ConfigTests(CLIFixture cli, ITestContextAccessor context) : CLITestBase
{
[Fact]
public async Task Should_CreateConfigFile_WhenDoesntExist()
{
await cli.ExecuteAsync("--print-config");

File.Exists(ConfigPath).Should().BeTrue();
}

[Fact]
public async Task Should_SaveArgumentsToConfigFile_WhenSetDefaultArgumentSet()
{
await cli.ExecuteAsync(
"--set-default",
"--print-config",
"-p", "parse pattern",
"--no-rename",
"--tv-pattern", "{Series} {Season:S00}{Episode:E00}",
"--movie-pattern", "{Title} {Year}",
"--windows-safe",
"--rename-subs",
"--replace", "a=b",
"--replace", "cde=fgh",
"-t",
"--no-tag",
"--no-cover",
"--manual",
"--extended-tagging",
"--apple-tagging",
"-l", "pt-BR",
"--search-language", "en-GB",
"--search-language", "en-US",
"-g",
"--include-adult",
"--remove-empty-folders"
);

var config = JsonSerializer.Deserialize<AutoTagConfig>(
await File.ReadAllTextAsync(ConfigPath, context.Current.CancellationToken),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
)!;

using (new AssertionScope())
{
config.ParsePattern.Should().Be("parse pattern");
config.RenameFiles.Should().BeFalse();
config.TVRenamePattern.Should().Be("{Series} {Season:S00}{Episode:E00}");
config.MovieRenamePattern.Should().Be("{Title} {Year}");
config.WindowsSafe.Should().BeTrue();
config.RenameSubtitles.Should().BeTrue();
config.FileNameReplaces.Should()
.BeEquivalentTo([new FileNameReplace("a", "b"), new FileNameReplace("cde", "fgh")]);
config.Mode.Should().Be(Mode.TV);
config.TagFiles.Should().BeFalse();
config.AddCoverArt.Should().BeFalse();
config.ManualMode.Should().BeTrue();
config.ExtendedTagging.Should().BeTrue();
config.AppleTagging.Should().BeTrue();
config.Language.Should().Be("pt-BR");
config.SearchLanguages.Should().BeEquivalentTo("en-GB", "en-US");
config.EpisodeGroup.Should().BeTrue();
config.IncludeAdult.Should().BeTrue();
config.RemoveEmptyFolders.Should().BeTrue();
}
}

[Fact]
public async Task Should_LoadConfigFromFile()
{
await cli.ExecuteAsync(
"--set-default",
"--print-config",
"--no-rename",
"--tv-pattern", "{Series}/{Season:S00}{Episode:E00}",
"--replace", "a=b",
"--replace", "cde=fgh"
);

var (output, _) = await cli.ExecuteAsync("--print-config");

var config = JsonSerializer.Deserialize<AutoTagConfig>(output)!;

config.RenameFiles.Should().BeFalse();
config.TVRenamePattern.Should().Be("{Series}/{Season:S00}{Episode:E00}");
config.FileNameReplaces.Should()
.BeEquivalentTo([new FileNameReplace("a", "b"), new FileNameReplace("cde", "fgh")]);
}
}
2 changes: 2 additions & 0 deletions AutoTag.CLI.Test/GlobalUsing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using AwesomeAssertions;
global using Xunit;
110 changes: 110 additions & 0 deletions AutoTag.CLI.Test/Helpers/CLIFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using AutoTag.CLI.Test.Helpers;
using CliWrap;
using CliWrap.Buffered;
using Spectre.Console.Cli.Testing;

[assembly: AssemblyFixture(typeof(CLIFixture))]
[assembly: CaptureConsole(CaptureOut = true, CaptureError = true)]

namespace AutoTag.CLI.Test.Helpers;

public partial class CLIFixture(ITestContextAccessor context) : IAsyncLifetime
{
private string _cliPublishOutput = null!;

[GeneratedRegex(@"\x1b\[[0-9;]*m")]
private static partial Regex AnsiCodeRegex { get; }

public async ValueTask InitializeAsync()
{
if (Debugger.IsAttached)
{
return;
}

var stdout = new StringBuilder();

string? outputPath = null;
var build = await Cli.Wrap("dotnet")
.WithWorkingDirectory(Path.Combine("..", "..", "..", ".."))
.WithArguments(["publish", "AutoTag.CLI", "-c", "Release"])
.WithStandardOutputPipe(PipeTarget.ToDelegate(l =>
{
stdout.AppendLine(l);

if (l.Contains("AutoTag.CLI ->"))
{
outputPath = Path.Combine(
l.Split("->", 2, StringSplitOptions.TrimEntries)[1],
$"autotag{(OperatingSystem.IsWindows() ? ".exe" : "")}"
);
}
}))
.WithStandardErrorPipe(PipeTarget.ToDelegate(context.Current.SendDiagnosticMessage))
.WithEnvironmentVariables(b => b.Set("TMDB_API_KEY", Environment.GetEnvironmentVariable("TMDB_API_KEY")))
.WithValidation(CommandResultValidation.None)
.ExecuteAsync();

if (!build.IsSuccess || string.IsNullOrEmpty(outputPath))
{
context.Current.SendDiagnosticMessage("CLI production build failed:\n{0}", stdout.ToString());

throw new Exception("CLI build failed");
}

_cliPublishOutput = outputPath;
}

public ValueTask DisposeAsync()
{
_cliPublishOutput = "";

return ValueTask.CompletedTask;
}

public async Task<(string, int)> ExecuteAsync(params string[] arguments)
{
if (context.Current.TestClassInstance is not CLITestBase cliTest)
{
throw new InvalidOperationException($"Test class must derive from {nameof(CLITestBase)}");
}

string[] args = [..arguments, "-c", cliTest.ConfigPath];

if (Debugger.IsAttached)
{
context.Current.SendDiagnosticMessage("Debugger detected, running CLI in-process");

var app = new CommandAppTester();
app.SetDefaultCommand<RootCommand>();

var appResult = await app.RunAsync(args);

return (RemoveAnsiColourCodes(appResult.Output), appResult.ExitCode);
}

if (string.IsNullOrEmpty(_cliPublishOutput))
{
throw new InvalidOperationException("CLI publish output not set");
}

var cmd = Cli.Wrap(_cliPublishOutput)
.WithArguments(args)
.WithValidation(CommandResultValidation.None);

var result = await cmd.ExecuteBufferedAsync();

if (!result.IsSuccess)
{
context.Current.SendDiagnosticMessage($"CLI returned non-zero exit code ({result.ExitCode})");
context.Current.SendDiagnosticMessage(result.StandardOutput);
}

return (RemoveAnsiColourCodes(result.StandardOutput), result.ExitCode);
}

private static string RemoveAnsiColourCodes(string input) => AnsiCodeRegex.Replace(input, "");
}
21 changes: 21 additions & 0 deletions AutoTag.CLI.Test/Helpers/CLITestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace AutoTag.CLI.Test.Helpers;

public abstract class CLITestBase : IDisposable
{
private readonly string _testDirectory;

protected CLITestBase()
{
_testDirectory = Path.Combine(".", $"test-files-{Random.Shared.Next()}");
Directory.CreateDirectory(_testDirectory);

ConfigPath = Path.Combine(_testDirectory, "conf.json");
FileSystem = new FileSystemBuilder(Path.Combine(_testDirectory, "filesystem"));
}

public string ConfigPath { get; }

protected FileSystemBuilder FileSystem { get; }

public void Dispose() => Directory.Delete(_testDirectory, true);
}
Loading
Loading