122 lines
No EOL
4 KiB
C#
122 lines
No EOL
4 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
|
|
namespace EllieBot.Modules.Music.Resolvers;
|
|
|
|
public sealed class LocalTrackResolver : ILocalTrackResolver
|
|
{
|
|
private static readonly HashSet<string> _musicExtensions = new[]
|
|
{
|
|
".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX",
|
|
".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V"
|
|
}.ToHashSet();
|
|
|
|
public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
|
|
{
|
|
if (!File.Exists(query))
|
|
return null;
|
|
|
|
var trackDuration = await Ffprobe.GetTrackDurationAsync(query);
|
|
return new SimpleTrackInfo(Path.GetFileNameWithoutExtension(query),
|
|
$"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}",
|
|
"https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png",
|
|
trackDuration,
|
|
MusicPlatform.Local,
|
|
$"\"{Path.GetFullPath(query)}\"");
|
|
}
|
|
|
|
public async IAsyncEnumerable<ITrackInfo> ResolveDirectoryAsync(string dirPath)
|
|
{
|
|
DirectoryInfo dir;
|
|
try
|
|
{
|
|
dir = new(dirPath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath);
|
|
yield break;
|
|
}
|
|
|
|
var files = dir.EnumerateFiles("*", SearchOption.AllDirectories)
|
|
.Where(x =>
|
|
{
|
|
if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System)
|
|
&& _musicExtensions.Contains(x.Extension.ToUpperInvariant()))
|
|
return true;
|
|
return false;
|
|
})
|
|
.ToList();
|
|
|
|
var firstFile = files.FirstOrDefault()?.FullName;
|
|
if (firstFile is null)
|
|
yield break;
|
|
|
|
var firstData = await ResolveByQueryAsync(firstFile);
|
|
if (firstData is not null)
|
|
yield return firstData;
|
|
|
|
var fileChunks = files.Skip(1).Chunk(10);
|
|
foreach (var chunk in fileChunks)
|
|
{
|
|
var part = await chunk.Select(x => ResolveByQueryAsync(x.FullName)).WhenAll();
|
|
|
|
// nullable reference types being annoying
|
|
foreach (var p in part)
|
|
{
|
|
if (p is null)
|
|
continue;
|
|
|
|
yield return p;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class Ffprobe
|
|
{
|
|
public static async Task<TimeSpan> GetTrackDurationAsync(string query)
|
|
{
|
|
query = query.Replace("\"", "");
|
|
|
|
try
|
|
{
|
|
using var p = Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = "ffprobe",
|
|
Arguments =
|
|
$"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"",
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
StandardOutputEncoding = Encoding.UTF8,
|
|
StandardErrorEncoding = Encoding.UTF8,
|
|
CreateNoWindow = true
|
|
});
|
|
|
|
if (p is null)
|
|
return TimeSpan.Zero;
|
|
|
|
var data = await p.StandardOutput.ReadToEndAsync();
|
|
if (double.TryParse(data, out var seconds))
|
|
return TimeSpan.FromSeconds(seconds);
|
|
|
|
var errorData = await p.StandardError.ReadToEndAsync();
|
|
if (!string.IsNullOrWhiteSpace(errorData))
|
|
Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData);
|
|
|
|
return TimeSpan.Zero;
|
|
}
|
|
catch (Win32Exception)
|
|
{
|
|
Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message);
|
|
}
|
|
|
|
return TimeSpan.Zero;
|
|
}
|
|
} |