Skip to content

Commit

Permalink
Update to .Net 8, fix bug with new api change with graphql, more details
Browse files Browse the repository at this point in the history
  • Loading branch information
fxsth committed Jun 21, 2024
1 parent e8258ba commit a23df22
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 102 deletions.
77 changes: 35 additions & 42 deletions audiothek-client/ApiRequester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class ApiRequester
{
Query = @"
{
programSets {nodes {title, numberOfElements, nodeId, rowId, editorialCategory{title, id}}}
programSets {nodes {title, numberOfElements, nodeId, rowId, editorialCategory{title, id}, lastItemAdded}}
}"
};

Expand All @@ -23,14 +23,14 @@ private GraphQLRequest ProgramSetByNodeIdRequest(string nodeId)
return new GraphQLRequest
{
Query =
$"{{ programSetByNodeId(nodeId:\"{nodeId}\") {{ rowId, items{{nodes{{ title, audios{{downloadUrl}}}}}}}}}}"
$"{{ programSetByNodeId(nodeId:\"{nodeId}\") {{ rowId, items{{nodes{{ title, audios{{url, downloadUrl, allowDownload}}, assetId, isPublished, publishDate, episodeNumber, summary, description, duration}}}}}}}}"
};
}

public async Task<IEnumerable<Node>> GetAllProgramSets()
{
var graphQlResponse = await _graphQlClient.SendQueryAsync<Data>(AllProgramSetsRequest);
return graphQlResponse.Data.programSets.nodes.Where(x=>x.numberOfElements != null);
return graphQlResponse.Data.programSets.nodes.Where(x => x.numberOfElements != null);
}

public async Task<IEnumerable<Node>> GetFilesByNodeId(string nodeId)
Expand All @@ -40,61 +40,54 @@ public async Task<IEnumerable<Node>> GetFilesByNodeId(string nodeId)
return graphQlResponse.Data.programSetByNodeId.items.nodes;
}

public async Task DownloadAllFilesFromNode(Node parentNode, string path)
public async Task DownloadAllFilesFromNodes(IEnumerable<Node> nodes, string parentTitle, string path)
{
string outputDir = Path.Combine(path, MakeValidFileName(parentNode.title));
GraphQLRequest query = ProgramSetByNodeIdRequest(parentNode.nodeId);
var graphQlResponse = await _graphQlClient.SendQueryAsync<Data>(query);
foreach (var node in graphQlResponse.Data.programSetByNodeId.items.nodes)
string outputDir = Path.Combine(path, MakeValidFileName(parentTitle));
foreach (var node in nodes)
{
int i = 0;
foreach (var audio in node.audios)
{
i++;
string downloadUrl = audio.downloadUrl;
if(string.IsNullOrEmpty(downloadUrl) || string.IsNullOrEmpty(node.title))
continue;
string partNumberInFilename = node.audios.Count > 1 ? $" ({i})" : string.Empty;
string filename = $"{MakeValidFileName(node.title)}{partNumberInFilename}.mp3";
await Download(downloadUrl, Path.Combine(outputDir, filename));
}
await Download(node, outputDir);
}
}

private async Task<string> TryGetNodeIdByTitle(string title)
public async Task Download(Node node, string outputDir)
{
var graphQlResponse = await _graphQlClient.SendQueryAsync<Data>(AllProgramSetsRequest);
return graphQlResponse.Data.programSets.nodes.Where(x => x.title == title).Select(x => x.nodeId)
.First();
int i = 0;
var audios = node.audios.Where(x => x.downloadUrl != null);
foreach (var audio in audios)
{
i++;
string downloadUrl = audio.downloadUrl!;
if (string.IsNullOrEmpty(downloadUrl) || string.IsNullOrEmpty(node.title))
continue;
string partNumberInFilename = audios.Count() > 1 ? $" ({i})" : string.Empty;
string filename = $"{MakeValidFileName(node.title)}{partNumberInFilename}.mp3";
await Download(downloadUrl, Path.Combine(outputDir, filename));
}
}

private async Task Download(string? downloadUrl, string filePath)
{
try
string? dirPath = Path.GetDirectoryName(filePath);
Directory.CreateDirectory(dirPath);
var uri = new Uri(downloadUrl);
var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(200);
using (var s = await httpClient.GetStreamAsync(uri))
{
string dirPath = Path.GetDirectoryName(filePath);
string filename = Path.GetFileName(downloadUrl);
string localFilename = Path.Combine(dirPath, filename);
Directory.CreateDirectory(dirPath);
var url = new Uri(downloadUrl);
var httpClient = new HttpClient();
await httpClient.GetByteArrayAsync(url).ContinueWith(data =>
using (var fs = new FileStream(filePath, FileMode.OpenOrCreate))
{
File.WriteAllBytes(localFilename, data.Result);
});
}
catch (Exception e)
{
//ignored
await s.CopyToAsync(fs);
}
}
}
private static string MakeValidFileName( string name )

private static string MakeValidFileName(string name)
{
string invalidChars = System.Text.RegularExpressions.Regex.Escape( new string( System.IO.Path.GetInvalidFileNameChars() ) );
string invalidRegStr = string.Format( @"([{0}]*\.+$)|([{0}]+)", invalidChars );
string invalidChars =
System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()));
string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars);

return System.Text.RegularExpressions.Regex.Replace( name, invalidRegStr, "-" );
return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "-");
}
}
}
20 changes: 17 additions & 3 deletions audiothek-client/Models/Root.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
public class Data
using System.Globalization;

public class Data
{
public ProgramSets programSets { get; set; }
public ProgramSetByNodeId programSetByNodeId { get; set; }
Expand All @@ -16,10 +18,20 @@ public class Node
public int? numberOfElements { get; set; }
public string nodeId { get; set; }
public int rowId { get; set; }
public EditorialCategory editorialCategory { get; set; }
public string summary { get; set; }
public string description { get; set; }
public string assetId { get; set; }
public int duration { get; set; }
public bool isPublished { get; set; }
public int? episodeNumber { get; set; }
public DateTimeOffset? publishDate { get; set; }
public DateTimeOffset? lastItemAdded { get; set; }
public EditorialCategory? editorialCategory { get; set; }
public List<Audio> audios { get; set; }
public override string ToString()
{
if (lastItemAdded != null)
return $"{title} ({lastItemAdded.Value.Date.ToString("dd.M.yyyy", CultureInfo.InvariantCulture)})";
return title;
}
}
Expand All @@ -32,7 +44,9 @@ public class ProgramSets
// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
public class Audio
{
public string downloadUrl { get; set; }
public string? downloadUrl { get; set; }
public string url { get; set; }
public bool allowDownload { get; set; }
}


Expand Down
12 changes: 4 additions & 8 deletions audiothek-client/audiothek-client.csproj
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>audiothek_client</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GraphQL.Client" Version="5.1.0" />
<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" Version="5.1.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.2.2" />
</ItemGroup>

<ItemGroup>
<Folder Include="Api.Catalog" />
<PackageReference Include="GraphQL.Client" Version="6.1.0" />
<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" Version="6.1.0" />
<PackageReference Include="GraphQL.SystemTextJson" Version="7.8.0" />
</ItemGroup>

</Project>
170 changes: 123 additions & 47 deletions audiothekar-cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,133 @@
using audiothek_client;
using Spectre.Console;

AnsiConsole.Clear();
AnsiConsole.Write(new FigletText("Audiothekar").Color(Color.Blue));
ApiRequester apiRequester = new ApiRequester();
IEnumerable<Node> nodesOfAllCategories = (await apiRequester.GetAllProgramSets()).ToList();
IEnumerable<IGrouping<string, Node>> categories = nodesOfAllCategories.Select(x =>
{
if (x.editorialCategory == null)
x.editorialCategory = new EditorialCategory() { id = "", title = "Andere" };
return x;
}).GroupBy(x => x.editorialCategory.title);
bool downloadSelected = false;
Node selectedNode = null;
string selectedCategory = null;
while (!downloadSelected)
namespace audiothekar_cli;

public static class Program
{
selectedCategory = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Rubrik:")
.PageSize(15)
.AddChoices(categories.Select(x => x.Key)));

selectedNode = AnsiConsole.Prompt(
new SelectionPrompt<Node>()
.Title("Reihe:")
.PageSize(15)
.AddChoices(categories.Single(x => x.Key == selectedCategory).OrderBy(x => x.title)));

var nodesByNodeId = await apiRequester.GetFilesByNodeId(selectedNode.nodeId);
var nodesWithDownloadUrl = nodesByNodeId.Where(x => x.audios.FirstOrDefault()?.downloadUrl != null).ToList();
if (!nodesWithDownloadUrl.Any())
public static async Task Main(string[] args)
{
AnsiConsole.Write("No downloads available");
Console.ReadKey();
AnsiConsole.Clear();
AnsiConsole.Write(new FigletText("Audiothekar").Color(Color.Blue));
ApiRequester apiRequester = new ApiRequester();
IEnumerable<Node> nodesOfAllCategories = (await apiRequester.GetAllProgramSets()).ToList();
IEnumerable<IGrouping<string, Node>> categories = nodesOfAllCategories.Select(x =>
{
x.editorialCategory ??= new EditorialCategory() { id = "", title = "Andere" };
return x;
}).GroupBy(x => x.editorialCategory!.title).ToList();
bool downloadSelected = false;
Node? selectedNode = null;
List<Node> children = new List<Node>();
while (!downloadSelected)
{
string selectedCategory = SelectCategory(categories);

selectedNode = SelectSeries(categories.Single(x => x.Key == selectedCategory));

var items = await apiRequester.GetFilesByNodeId(selectedNode.nodeId);
children = FilterDownloadableNodes(items);
if (!children.Any())
{
AnsiConsole.Write("No downloads available");
Console.ReadKey();
}
else
{
AnsiConsole.Write(CreateTreeFromNodes(selectedNode, children));
downloadSelected = AnsiConsole.Confirm("Download starten?");
}

if (!downloadSelected)
AnsiConsole.Clear();
}

await DownloadAllFilesFromNodes(apiRequester, children, selectedNode!.title);
}
else

private static string SelectCategory(IEnumerable<IGrouping<string, Node>> categories)
{
AnsiConsole.Write(new Rows(nodesWithDownloadUrl.Select(x => new Text(x.title))));
downloadSelected = AnsiConsole.Confirm("Download starten?");
string selectedCategory = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Rubrik:")
.PageSize(15)
.AddChoices(categories.Select(x => x.Key).Order()));
return selectedCategory;
}

if (!downloadSelected)
AnsiConsole.Clear();
}
private static Node SelectSeries(IEnumerable<Node> nodes)
{
Node selectedNode = AnsiConsole.Prompt(
new SelectionPrompt<Node>()
.Title("Reihe:")
.PageSize(15)
.AddChoices(nodes.OrderByDescending(x => x.lastItemAdded)));
return selectedNode;
}

string path = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic);
Task downloadTask = apiRequester.DownloadAllFilesFromNode(selectedNode, path);
Console.WriteLine($"Download {selectedNode.title} nach {path}");
await AnsiConsole.Status().StartAsync("Downloading...", async ctx =>
{
ctx.Spinner(Spinner.Known.Star);
ctx.SpinnerStyle(Style.Parse("green"));
await downloadTask;
});
Console.WriteLine("-------------");
private static List<Node> FilterDownloadableNodes(IEnumerable<Node> nodes)
{
return nodes
.Where(x => x.audios.Any(audio => audio.downloadUrl != null))
.GroupBy(x => x.assetId)
.Select(y => y.OrderByDescending(a => a.publishDate).First())
.OrderBy(x => x.episodeNumber).ToList();
}

private static Tree CreateTreeFromNodes(Node root, IReadOnlyCollection<Node> children)
{
var tree = new Tree(root.editorialCategory!.title);

var item = tree.AddNode(root.title);
item.AddNode(new Text($"{children.Count} Items"));
var table = new Table()
.RoundedBorder()
.AddColumn("Name")
.AddColumn("Dauer");
item.AddNode(table);
foreach (var child in children)
{
table.AddRow(child.title, TimeSpan.FromSeconds(child.duration).ToString());
}

return tree;
}

public static async Task DownloadAllFilesFromNodes(ApiRequester apiRequester, IEnumerable<Node> nodes,
string parentTitle)
{
string outputRootDir = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic);
string outputDir = Path.Combine(outputRootDir, MakeValidFileName(parentTitle));
Console.WriteLine($"Download {parentTitle} nach {outputDir}");
await AnsiConsole.Status().StartAsync("Downloading...", async ctx =>
{
ctx.Spinner(Spinner.Known.Star);
ctx.SpinnerStyle(Style.Parse("green"));
foreach (var node in nodes)
{
try
{
ctx.Status = $"Downloading '{node.title}'";
await apiRequester.Download(node, outputDir);
}
catch (Exception e)
{
AnsiConsole.Markup($"Download von '{node.title}' schlug fehl: {e.Message}");
if (!node.isPublished)
AnsiConsole.Markup($"Mögliche Ursache: Diese Resource ist (noch) nicht veröffentlicht.");
AnsiConsole.Markup($"Setze restliche Downloads fort...");
}
}
});
AnsiConsole.Markup(":check_mark_button: Completed.");
}

private static string MakeValidFileName(string name)
{
string invalidChars =
System.Text.RegularExpressions.Regex.Escape(new string(Path.GetInvalidFileNameChars()));
string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars);

return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "-");
}
}
4 changes: 2 additions & 2 deletions audiothekar-cli/audiothekar-cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>audiothekar_cli</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand All @@ -13,7 +13,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.46.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
</ItemGroup>

</Project>

0 comments on commit a23df22

Please sign in to comment.