Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ FodyWeavers.xsd
## Visual studio for Mac
##


# globs
Makefile.in
*.userprefs
Expand All @@ -438,7 +437,6 @@ test-results/
# Icon must end with two \r
Icon


# Thumbnails
._*

Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableRoslynAnalyzers": true,
"dotnet.defaultSolution": "media-encoding.sln"
"dotnet.defaultSolution": "RipSharp.sln",
"chat.tools.terminal.autoApprove": {
"gh": true
}
}
2 changes: 0 additions & 2 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,6 @@ dotnet run --project src/RipSharp -- --mode tv --disc disc:1 --title "Friends" -

## Advanced Examples



## Specific Scenarios

### 4K UltraHD Blu-Ray
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,51 @@ macOS:

If no config file exists, RipSharp creates one in the first personal location above (for example, `$XDG_CONFIG_HOME/ripsharp/config.yaml` on Linux).

## Theming

Themes are loaded from a YAML file located under a `themes` subdirectory in the config directory and bound to options. On startup, RipSharp writes bundled themes into that directory if they are missing and does not overwrite existing files. Set the theme name in your config file:

```yaml
theme: "catppuccin mocha"
```

Built-in themes:

- "catppuccin latte"
- "catppuccin frappe"
- "catppuccin macchiato"
- "catppuccin mocha"
- "dracula"
- "nord"
- "tokyo-night"
- "gruvbox dark"
- "gruvbox light"

Theme file format (YAML):

```yaml
theme:
colors:
success: "#94e2d5"
error: "#f38ba8"
warning: "#f9e2af"
info: "#89b4fa"
accent: "#89dceb"
muted: "#6c7086"
highlight: "#cba6f7"
emojis:
success: "✓"
error: "❌"
warning: "⚠️"
insert_disc: "💿"
disc_detected: "📀"
scan: "🔍"
disc_type: "💽"
title_found: "🎞️"
tv: "📺"
movie: "🎬"
```

## Building

```bash
Expand Down
74 changes: 74 additions & 0 deletions src/RipSharp.Tests/Core/ThemeFileLocatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.IO;

using AwesomeAssertions;

using Xunit;

namespace BugZapperLabs.RipSharp.Tests.Core;

public class ThemeFileLocatorTests
{
[Fact]
public void ResolveThemePath_WhenRelative_ReturnsThemePathUnderConfigThemes()
{
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");

var result = ThemeFileLocator.ResolveThemePath("custom.yaml", configPath);

result.Should().Be(Path.Combine("/home/tester", ".config", "ripsharp", "themes", "custom.yaml"));
}

[Theory]
[InlineData("catppuccin mocha", "catppuccin-mocha.yaml")]
[InlineData("catppuccin-mocha", "catppuccin-mocha.yaml")]
[InlineData("catppuccin_mocha", "catppuccin-mocha.yaml")]
public void ResolveThemePath_WhenNameProvided_NormalizesToFileName(string themeName, string expectedFileName)
{
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");

var result = ThemeFileLocator.ResolveThemePath(themeName, configPath);

result.Should().Be(Path.Combine("/home/tester", ".config", "ripsharp", "themes", expectedFileName));
}

[Fact]
public void ResolveThemePath_WhenMissing_UsesDefaultThemeFile()
{
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");

var result = ThemeFileLocator.ResolveThemePath(null, configPath);

result.Should().Be(Path.Combine("/home/tester", ".config", "ripsharp", "themes", "catppuccin-mocha.yaml"));
}

[Fact]
public void EnsureBundledThemeFiles_WritesThemesIntoConfigThemesDirectory()
{
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");
var files = new Dictionary<string, string>();
var directories = new HashSet<string>();
var bundledThemes = new Dictionary<string, string>
{
["catppuccin-mocha.yaml"] = "theme:\n colors:\n",
["catppuccin-latte.yaml"] = "theme:\n colors:\n"
};

ThemeFileLocator.EnsureBundledThemeFiles(
configPath,
path => files.ContainsKey(path),
(path, content) => files[path] = content,
path => directories.Add(path),
bundledThemes);

var expectedThemeDir = Path.Combine("/home/tester", ".config", "ripsharp", "themes");
var expectedMochaPath = Path.Combine(expectedThemeDir, "catppuccin-mocha.yaml");
var expectedLattePath = Path.Combine(expectedThemeDir, "catppuccin-latte.yaml");

directories.Should().Contain(expectedThemeDir);
files.Should().ContainKey(expectedMochaPath);
files.Should().ContainKey(expectedLattePath);
files[expectedMochaPath].Should().NotBeNullOrWhiteSpace();
files[expectedLattePath].Should().NotBeNullOrWhiteSpace();
}
}
12 changes: 6 additions & 6 deletions src/RipSharp.Tests/Metadata/MetadataServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async Task LookupAsync_Fallbacks_WhenNoApiKeys()
Environment.SetEnvironmentVariable("TMDB_API_KEY", null);
var notifier = Substitute.For<IConsoleWriter>();
var providers = new List<IMetadataProvider>();
var svc = new MetadataService(providers, notifier);
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());

var md = await svc.LookupAsync("SIMPSONS_WS", isTv: false, year: null);

Expand All @@ -46,7 +46,7 @@ public async Task LookupAsync_ReturnsFromFirstProvider_WhenMatch()
provider2.Name.Returns("Provider2");

var providers = new List<IMetadataProvider> { provider1, provider2 };
var svc = new MetadataService(providers, notifier);
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());

var result = await svc.LookupAsync("test", isTv: false, year: null);

Expand All @@ -70,7 +70,7 @@ public async Task LookupAsync_TriesSecondProvider_WhenFirstReturnsNull()
provider2.LookupAsync("test", false, null).Returns(new ContentMetadata { Title = "Test Movie", Year = 2021, Type = "movie" });

var providers = new List<IMetadataProvider> { provider1, provider2 };
var svc = new MetadataService(providers, notifier);
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());

var result = await svc.LookupAsync("test", isTv: false, year: null);

Expand All @@ -91,7 +91,7 @@ public async Task LookupAsync_UsesTitleVariations_WhenOriginalFails()
provider.LookupAsync("MOVIE_TITLE", Arg.Any<bool>(), Arg.Any<int?>()).Returns(new ContentMetadata { Title = "Movie Title", Year = 2023, Type = "movie" });

var providers = new List<IMetadataProvider> { provider };
var svc = new MetadataService(providers, notifier);
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());

var result = await svc.LookupAsync("MOVIE_TITLE_2023", isTv: false, year: null);

Expand All @@ -112,7 +112,7 @@ public async Task LookupAsync_ShowsDifferentMessage_ForSimplifiedTitle()
provider.LookupAsync("SIMPSONS", Arg.Any<bool>(), Arg.Any<int?>()).Returns(new ContentMetadata { Title = "The Simpsons", Year = 1989, Type = "tv" });

var providers = new List<IMetadataProvider> { provider };
var svc = new MetadataService(providers, notifier);
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());

await svc.LookupAsync("SIMPSONS_WS", isTv: true, year: null);

Expand All @@ -131,7 +131,7 @@ public async Task LookupAsync_ShowsNormalMessage_ForOriginalTitle()
provider.LookupAsync("Test Movie", Arg.Any<bool>(), Arg.Any<int?>()).Returns(new ContentMetadata { Title = "Test Movie", Year = 2020, Type = "movie" });

var providers = new List<IMetadataProvider> { provider };
var svc = new MetadataService(providers, notifier);
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());

await svc.LookupAsync("Test Movie", isTv: false, year: null);

Expand Down
3 changes: 2 additions & 1 deletion src/RipSharp.Tests/Services/DiscRipperTitleSuffixTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ private static DiscRipper CreateRipper(IEncoderService encoder)
var userPrompt = Substitute.For<IUserPrompt>();
var episodeTitles = Substitute.For<ITvEpisodeTitleProvider>();
var progressDisplay = Substitute.For<IProgressDisplay>();
var theme = ThemeProvider.CreateDefault();

return new DiscRipper(scanner, encoder, metadataService, makeMkv, notifier, userPrompt, episodeTitles, progressDisplay);
return new DiscRipper(scanner, encoder, metadataService, makeMkv, notifier, userPrompt, episodeTitles, progressDisplay, theme);
}

private static async Task<List<object>> InvokeBuildTitlePlansAsync(
Expand Down
42 changes: 42 additions & 0 deletions src/RipSharp.Tests/Utilities/ThemeProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.Extensions.Options;

using Spectre.Console;

using Xunit;

namespace BugZapperLabs.RipSharp.Tests.Utilities;

public class ThemeProviderTests
{
[Fact]
public void Colors_AreLoadedFromOptions()
{
var options = Options.Create(new ThemeOptions
{
Colors = new ThemeColors
{
Success = "#010203"
}
});

var provider = new ThemeProvider(options);

provider.SuccessColor.Should().Be(new Color(1, 2, 3));
}

[Fact]
public void Emojis_AreLoadedFromOptions()
{
var options = Options.Create(new ThemeOptions
{
Emojis = new ThemeEmojis
{
Warning = "!"
}
});

var provider = new ThemeProvider(options);

provider.Emojis.Warning.Should().Be("!");
}
}
7 changes: 4 additions & 3 deletions src/RipSharp/Core/ConfigFileLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ internal static class ConfigFileLocator
" default_path: \"disc:0\"\n" +
" default_temp_dir: \"/tmp/makemkv\"\n\n" +
"output:\n" +
" movies_dir: \"~/Movies\"\n" +
" tv_dir: \"~/TV\"\n\n" +
" movies_dir: \"~/Videos/Movies\"\n" +
" tv_dir: \"~/Videos/TV\"\n\n" +
"encoding:\n" +
" include_english_subtitles: true\n" +
" include_stereo_audio: true\n" +
" include_surround_audio: true\n\n" +
"metadata:\n" +
" lookup_enabled: true\n";
" lookup_enabled: true\n\n" +
"theme: \"catppuccin mocha\"\n";

internal static ConfigSearchContext CreateContext()
{
Expand Down
33 changes: 28 additions & 5 deletions src/RipSharp/Core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;


namespace BugZapperLabs.RipSharp.Core;

public class Program
Expand Down Expand Up @@ -36,16 +35,18 @@ public static async Task<int> Main(string[] args)
private static async Task<int> RunAsync(string[] args, CursorManager cursorManager)
{
var options = RipOptions.ParseArgs(args);
var defaultTheme = ThemeProvider.CreateDefault();
var consoleWriter = new ConsoleWriter(defaultTheme);

if (options.ShowHelp)
{
RipOptions.DisplayHelp(new ConsoleWriter());
RipOptions.DisplayHelp(consoleWriter);
return 0;
}

if (options.ShowVersion)
{
Console.WriteLine($"ripsharp {GetVersion()}");
consoleWriter.Plain($"ripsharp {GetVersion()}");
return 0;
}

Expand All @@ -56,7 +57,7 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag

if (missingTools.Count > 0)
{
var prereqWriter = new ConsoleWriter();
var prereqWriter = consoleWriter;
prereqWriter.Error("Missing required prerequisites:");
foreach (var tool in missingTools)
{
Expand Down Expand Up @@ -105,11 +106,32 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag
cfg.AddYamlFile(configPath, optional: false, reloadOnChange: true);
}

ThemeFileLocator.EnsureBundledThemeFiles(
configPath,
File.Exists,
File.WriteAllText,
path => Directory.CreateDirectory(path));

var tempConfig = cfg.Build();
var themeSetting = tempConfig.GetValue<string>("theme");
if (string.IsNullOrWhiteSpace(themeSetting))
{
themeSetting = tempConfig.GetSection("theme").GetValue<string>("path");
}
var resolvedThemePath = ThemeFileLocator.ResolveThemePath(themeSetting, configPath);

if (!string.IsNullOrWhiteSpace(resolvedThemePath))
{
cfg.AddYamlFile(resolvedThemePath, optional: true, reloadOnChange: true);
}

cfg.AddEnvironmentVariables();
})
.ConfigureServices((ctx, services) =>
{
services.Configure<AppConfig>(ctx.Configuration);
services.Configure<ThemeOptions>(ctx.Configuration.GetSection("theme"));
services.AddSingleton<IThemeProvider, ThemeProvider>();
services.AddSingleton<IConsoleWriter, ConsoleWriter>();
services.AddSingleton<IProgressDisplay, SpectreProgressDisplay>();
services.AddSingleton<IUserPrompt, ConsoleUserPrompt>();
Expand Down Expand Up @@ -155,6 +177,7 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag

var ripper = host.Services.GetRequiredService<IDiscRipper>();
var writer = host.Services.GetRequiredService<IConsoleWriter>();
var theme = host.Services.GetRequiredService<IThemeProvider>();

List<string> files;
try
Expand All @@ -163,7 +186,7 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag
}
catch (OperationCanceledException)
{
writer.Warning("\n⚠️ Operation interrupted by user");
writer.Warning($"\n{theme.Emojis.Warning} Operation interrupted by user");
return 130;
}

Expand Down
Loading