Import existing commands
This commit is contained in:
parent
692c50c377
commit
af2d4d6139
10 changed files with 460 additions and 0 deletions
3
src/Commands/.editorconfig
Executable file
3
src/Commands/.editorconfig
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[*.{cs,vb}]
|
||||||
|
dotnet_diagnostic.CA1822.severity = none
|
||||||
|
dotnet_diagnostic.IDE0060.severity = none
|
||||||
98
src/Commands/Library/ArrangeCommand.cs
Executable file
98
src/Commands/Library/ArrangeCommand.cs
Executable file
|
|
@ -0,0 +1,98 @@
|
||||||
|
using ConsoleAppFramework;
|
||||||
|
|
||||||
|
namespace MediaOrganizer.Commands.Library;
|
||||||
|
|
||||||
|
[RegisterCommands("library")]
|
||||||
|
internal class ArrangeCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Arranges files next to mapFile into targetDirectory.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapFile">A file with "="-separated identifiers.</param>
|
||||||
|
/// <param name="targetDirectory">Target directory to store value-part of mapFile in.</param>
|
||||||
|
[Command("arrange")]
|
||||||
|
public async Task<int> Execute(
|
||||||
|
ConsoleAppContext context,
|
||||||
|
string mapFile,
|
||||||
|
string targetDirectory,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (new FileInfo(mapFile) is not
|
||||||
|
{
|
||||||
|
Exists: true
|
||||||
|
} mapFileInfo)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Map File {mapFile} not found", nameof(mapFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectoryInfo targetDirectoryInfo = new(targetDirectory);
|
||||||
|
targetDirectoryInfo.Create();
|
||||||
|
|
||||||
|
bool error = false;
|
||||||
|
using var reader = mapFileInfo.OpenText();
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken) is { } line)
|
||||||
|
{
|
||||||
|
if (line.AsMemory().Trim() is not
|
||||||
|
{
|
||||||
|
IsEmpty: false
|
||||||
|
} lineMemory)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sep = lineMemory.Span.LastIndexOf('=');
|
||||||
|
if (lineMemory[(sep + 1)..].Trim() is not
|
||||||
|
{
|
||||||
|
IsEmpty: false
|
||||||
|
} targetMemory)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourcePrefix = lineMemory[..sep].Trim().ToString();
|
||||||
|
var targetFile = targetMemory.ToString();
|
||||||
|
var targetFileBaseName = Path.Join(targetDirectoryInfo.FullName, targetFile);
|
||||||
|
var targetFileDirectory = Path.GetDirectoryName(targetFileBaseName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(targetFileDirectory!);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
ConsoleApp.LogError($"Create target directory for {sourcePrefix} in {targetFileDirectory}: {e.Message}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var file in Directory.EnumerateFiles(mapFileInfo.DirectoryName!, $"{sourcePrefix}*"))
|
||||||
|
{
|
||||||
|
var relative = Path.GetRelativePath(mapFileInfo.DirectoryName!, file);
|
||||||
|
var extension = relative[sourcePrefix.Length..];
|
||||||
|
var target = targetFileBaseName + extension;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(target))
|
||||||
|
{
|
||||||
|
File.Copy(file, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
ConsoleApp.LogError($"Copy file {relative} to {target}: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
ConsoleApp.LogError($"Listing directory {mapFileInfo.DirectoryName}: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/Commands/Prepare/ArchiveCommand.cs
Executable file
119
src/Commands/Prepare/ArchiveCommand.cs
Executable file
|
|
@ -0,0 +1,119 @@
|
||||||
|
using ConsoleAppFramework;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaOrganizer.Commands.Prepare;
|
||||||
|
|
||||||
|
[RegisterCommands("prepare")]
|
||||||
|
internal class ArchiveCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a map file from a youtube-dl archive file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapFile"></param>
|
||||||
|
/// <param name="archiveFile"></param>
|
||||||
|
/// <param name="episode"></param>
|
||||||
|
/// <param name="episodeNameFormat"></param>
|
||||||
|
[Command("archive")]
|
||||||
|
public async Task Execute(
|
||||||
|
ConsoleAppContext context,
|
||||||
|
string mapFile,
|
||||||
|
string archiveFile,
|
||||||
|
int episode = 1,
|
||||||
|
string episodeNameFormat = "S01E{Episode:00}",
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (new FileInfo(archiveFile) is not
|
||||||
|
{
|
||||||
|
Exists: true
|
||||||
|
} archiveFileInfo)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Archive file {archiveFile} has to exist", nameof(archiveFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo mapFileInfo = new(mapFile);
|
||||||
|
LogValuesFormat format = new(episodeNameFormat);
|
||||||
|
HashSet<string> skip = [];
|
||||||
|
using var mapStream = mapFileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
if (mapStream.Length > 0)
|
||||||
|
{
|
||||||
|
using StreamReader reader = new(mapStream, leaveOpen: true);
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken) is { } line)
|
||||||
|
{
|
||||||
|
if (line.AsMemory().Trim() is not
|
||||||
|
{
|
||||||
|
IsEmpty: false
|
||||||
|
} memory)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sep = memory.Span.LastIndexOf('=');
|
||||||
|
if (sep == -1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = memory[..sep];
|
||||||
|
var path = key.Span.LastIndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]);
|
||||||
|
if (path != -1)
|
||||||
|
{
|
||||||
|
key = key[(path + 1)..];
|
||||||
|
}
|
||||||
|
|
||||||
|
skip.Add(key.Trim().ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var relativePath = Path.GetRelativePath(
|
||||||
|
mapFileInfo.DirectoryName!,
|
||||||
|
archiveFileInfo.DirectoryName!);
|
||||||
|
if (relativePath is ".")
|
||||||
|
{
|
||||||
|
relativePath = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format.ValueNames.Count > 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Episode format has too many unique format values.", nameof(episodeNameFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatArgs = new object[format.ValueNames.Count];
|
||||||
|
EpisodeFormat? episodeFormat = null;
|
||||||
|
for (int i = 0; i < format.ValueNames.Count; i++)
|
||||||
|
{
|
||||||
|
string formatKey;
|
||||||
|
formatArgs[i] = (formatKey = format.ValueNames[i]) switch
|
||||||
|
{
|
||||||
|
"Episode" => episodeFormat = new()
|
||||||
|
{
|
||||||
|
Episode = episode
|
||||||
|
},
|
||||||
|
_ => throw new ArgumentException($"Format value {formatKey} unrecognized.", nameof(episodeNameFormat)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using StreamWriter mapWriter = new(mapStream, leaveOpen: true);
|
||||||
|
using var archiveReader = archiveFileInfo.OpenText();
|
||||||
|
while (await archiveReader.ReadLineAsync(cancellationToken) is { } line)
|
||||||
|
{
|
||||||
|
if (ID_MATCHER.Match(line) is not { Success: true } match)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = match.Groups["id"].Value;
|
||||||
|
if (skip.Contains(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var written = string.Format(null, format.Format, formatArgs);
|
||||||
|
mapWriter.WriteLine($"{Path.Join(relativePath, id)}={written}");
|
||||||
|
if (episodeFormat is not null)
|
||||||
|
{
|
||||||
|
episodeFormat.Episode++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Commands/Prepare/EpisodeFormat.cs
Normal file
20
src/Commands/Prepare/EpisodeFormat.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace MediaOrganizer.Commands.Prepare;
|
||||||
|
|
||||||
|
internal sealed record class EpisodeFormat : ISpanFormattable, IUtf8SpanFormattable
|
||||||
|
{
|
||||||
|
private int _episode;
|
||||||
|
|
||||||
|
public ref int Episode => ref _episode;
|
||||||
|
|
||||||
|
public string ToString(string? format, IFormatProvider? formatProvider) => _episode.ToString(formatProvider);
|
||||||
|
|
||||||
|
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
return _episode.TryFormat(utf8Destination, out bytesWritten, format, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
return _episode.TryFormat(destination, out charsWritten, format, provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/Commands/Prepare/MapCommand.cs
Executable file
113
src/Commands/Prepare/MapCommand.cs
Executable file
|
|
@ -0,0 +1,113 @@
|
||||||
|
using ConsoleAppFramework;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
|
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaOrganizer.Commands.Prepare;
|
||||||
|
|
||||||
|
[RegisterCommands("prepare")]
|
||||||
|
internal class MapCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a mapFile, and applies a format onto all files.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapFile">Path to map file.</param>
|
||||||
|
/// <param name="include">Include pattern to include relative to mapFile directory.</param>
|
||||||
|
/// <param name="episode">Episode number to start with for new entries in mapFile.</param>
|
||||||
|
/// <param name="episodeNameFormat">Format to store mapping in mapFile. Possible options:
|
||||||
|
/// - Episode: integer
|
||||||
|
/// </param>
|
||||||
|
[Command("map")]
|
||||||
|
public async Task Execute(
|
||||||
|
ConsoleAppContext context,
|
||||||
|
string mapFile,
|
||||||
|
string[] include,
|
||||||
|
int episode = 1,
|
||||||
|
string episodeNameFormat = "S01E{Episode:00}",
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
FileInfo mapFileInfo = new(mapFile);
|
||||||
|
Matcher matcher = new(StringComparison.OrdinalIgnoreCase);
|
||||||
|
matcher.AddIncludePatterns(include);
|
||||||
|
LogValuesFormat format = new(episodeNameFormat);
|
||||||
|
SortedSet<string> episodes = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var item in matcher.Execute(new DirectoryInfoWrapper(mapFileInfo.Directory!)).Files)
|
||||||
|
{
|
||||||
|
var nameWithoutExtension = StripExtension(item.Path);
|
||||||
|
if (!episodes.AnyStartsWith(nameWithoutExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
episodes.RemovePrefix(nameWithoutExtension, StringComparison.OrdinalIgnoreCase);
|
||||||
|
episodes.Add(nameWithoutExtension.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var mapStream = mapFileInfo.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
if (mapStream.Length > 0)
|
||||||
|
{
|
||||||
|
using StreamReader reader = new(mapStream, leaveOpen: true);
|
||||||
|
while (await reader.ReadLineAsync(cancellationToken) is { } line)
|
||||||
|
{
|
||||||
|
if (line.AsMemory().Trim() is not
|
||||||
|
{
|
||||||
|
IsEmpty: false
|
||||||
|
} memory)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sep = memory.Span.LastIndexOf('=');
|
||||||
|
if (sep == -1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
episodes.Remove(memory[..sep].Trim().NormalizePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format.ValueNames.Count > 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Episode format has too many unique format values.", nameof(episodeNameFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatArgs = new object[format.ValueNames.Count];
|
||||||
|
EpisodeFormat? episodeFormat = null;
|
||||||
|
for (int i = 0; i < format.ValueNames.Count; i++)
|
||||||
|
{
|
||||||
|
string formatKey;
|
||||||
|
formatArgs[i] = (formatKey = format.ValueNames[i]) switch
|
||||||
|
{
|
||||||
|
"Episode" => episodeFormat = new()
|
||||||
|
{
|
||||||
|
Episode = episode
|
||||||
|
},
|
||||||
|
_ => throw new ArgumentException($"Format value {formatKey} unrecognized.", nameof(episodeNameFormat)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
object[] formatList = [format];
|
||||||
|
using StreamWriter mapWriter = new(mapStream, leaveOpen: true);
|
||||||
|
foreach (var item in episodes)
|
||||||
|
{
|
||||||
|
var written = string.Format(null, format.Format, formatArgs);
|
||||||
|
mapWriter.WriteLine($"{item}={written}");
|
||||||
|
if (episodeFormat is not null)
|
||||||
|
{
|
||||||
|
episodeFormat.Episode++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ReadOnlySpan<char> StripExtension(in ReadOnlySpan<char> path)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(path);
|
||||||
|
int i;
|
||||||
|
if ((i = fileName.LastIndexOf('.')) == -1)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path[..^(fileName.Length - i)];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Microsoft/Extensions/Logging/LogValuesFormat.cs
Normal file
22
src/Microsoft/Extensions/Logging/LogValuesFormat.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
public readonly struct LogValuesFormat(string format)
|
||||||
|
{
|
||||||
|
private readonly LogValuesFormatter _instance = Create(format);
|
||||||
|
|
||||||
|
public CompositeFormat Format => GetFormat(_instance);
|
||||||
|
|
||||||
|
public List<string> ValueNames => GetValueNames(_instance);
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
|
||||||
|
private static extern LogValuesFormatter Create(string format);
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = nameof(LogValuesFormatter._format))]
|
||||||
|
private static extern ref CompositeFormat GetFormat(LogValuesFormatter instance);
|
||||||
|
|
||||||
|
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = nameof(LogValuesFormatter._valueNames))]
|
||||||
|
private static extern ref List<string> GetValueNames(LogValuesFormatter instance);
|
||||||
|
}
|
||||||
|
|
@ -90,4 +90,13 @@ ConsoleApp.Create().Run(args);
|
||||||
|
|
||||||
internal static partial class Program
|
internal static partial class Program
|
||||||
{
|
{
|
||||||
|
public static Regex FILEID_MATCHER => field ??= FileIdMatcherRegex();
|
||||||
|
|
||||||
|
public static Regex ID_MATCHER => field ??= IdMatcherRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\[(?<id>[^\[]+)\](?=\..+$)")]
|
||||||
|
private static partial Regex FileIdMatcherRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("^(?<parser>.+?) (?<id>.+?)$")]
|
||||||
|
private static partial Regex IdMatcherRegex();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
src/System/Collections/Generic/PathExtensions.cs
Normal file
46
src/System/Collections/Generic/PathExtensions.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
namespace System.Collections.Generic;
|
||||||
|
|
||||||
|
public static class PathExtensions
|
||||||
|
{
|
||||||
|
public static bool AnyStartsWith(this SortedSet<string> files, ReadOnlySpan<char> prefix, StringComparison comparison)
|
||||||
|
{
|
||||||
|
using var enumerator = files.GetEnumerator();
|
||||||
|
return AnyStartsWith(enumerator, prefix, comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int RemovePrefix(this SortedSet<string> files, ReadOnlySpan<char> prefix, StringComparison comparison)
|
||||||
|
{
|
||||||
|
List<string> matches = [];
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file.StartsWith(prefix, comparison))
|
||||||
|
{
|
||||||
|
matches.Add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int actuallyRemoved = 0;
|
||||||
|
for (int i = matches.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (files.Remove(matches[i]))
|
||||||
|
{
|
||||||
|
actuallyRemoved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actuallyRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AnyStartsWith<T>(in T files, ReadOnlySpan<char> prefix, StringComparison comparison) where T : IEnumerator<string>
|
||||||
|
{
|
||||||
|
while (files.MoveNext())
|
||||||
|
{
|
||||||
|
if (files.Current.StartsWith(prefix, comparison))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/System/IO/StringExtensions.cs
Normal file
21
src/System/IO/StringExtensions.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
namespace System.IO;
|
||||||
|
|
||||||
|
public static partial class StringExtensions
|
||||||
|
{
|
||||||
|
public static string NormalizePath(in this ReadOnlyMemory<char> path)
|
||||||
|
{
|
||||||
|
return string.Create(path.Length, path, (c, path) =>
|
||||||
|
{
|
||||||
|
var writer = c.GetEnumerator();
|
||||||
|
foreach (ref readonly var s in path.Span)
|
||||||
|
{
|
||||||
|
writer.MoveNext();
|
||||||
|
ref var t = ref writer.Current;
|
||||||
|
if ((t = s) == Path.DirectorySeparatorChar)
|
||||||
|
{
|
||||||
|
t = Path.AltDirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,13 @@
|
||||||
<TrimMode>partial</TrimMode>
|
<TrimMode>partial</TrimMode>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PublicizerRuntimeStrategies>Unsafe</PublicizerRuntimeStrategies>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Properties\appsettings.json" />
|
<EmbeddedResource Include="Properties\appsettings.json" />
|
||||||
|
<Publicize Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -28,6 +33,10 @@
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Krafs.Publicizer" Version="2.3.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue