Initial Commit

master
Harald Wolff 2024-01-27 19:47:17 +01:00
commit 7beedced84
13 changed files with 443 additions and 0 deletions

5
.gitignore vendored 100644
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.ln.podget.iml
/contentModel.xml
/modules.xml
/projectSettingsUpdater.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

17
ln.podget.sln 100644
View File

@ -0,0 +1,17 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.podget", "ln.podget\ln.podget.csproj", "{97F573B1-9EBC-48F0-95BB-2AAEF06E85F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{97F573B1-9EBC-48F0-95BB-2AAEF06E85F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{97F573B1-9EBC-48F0-95BB-2AAEF06E85F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97F573B1-9EBC-48F0-95BB-2AAEF06E85F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97F573B1-9EBC-48F0-95BB-2AAEF06E85F4}.Release|Any CPU.Build.0 = Release|Any CPU
{97F573B1-9EBC-48F0-95BB-2AAEF06E85F4}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,44 @@
using System.Text.RegularExpressions;
namespace ln.podget;
public static class PathHelper
{
public static string GetValidPathElement(string pathElement)
{
return Regex.Replace(pathElement, "[^a-zA-Z0-9_ äöü]", "_");
}
public static string GetValidPathElement(string folderPath, string pathElement)
{
string cleanElement = Regex.Replace(pathElement, "[^a-zA-Z0-9_ äöü]", "_");
int remaining = 254 - folderPath.Length - cleanElement.Length;
if (remaining < 0)
{
string ext = Path.GetExtension(pathElement);
cleanElement = cleanElement.Substring(0, cleanElement.Length + remaining - ext.Length) + ext;
}
return cleanElement;
}
public static void EnsureDirectory(string path)
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
public static string GetTypeExtension(string type)
{
if (_typeExtensions.TryGetValue(type, out string ext))
return ext;
return "bin";
}
private static Dictionary<string, string> _typeExtensions = new Dictionary<string, string>()
{
{ "audio/mpeg", "mp3" },
{ "audio/mp3", "mp3" },
{ "audio/ogg", "ogg" },
};
}

View File

@ -0,0 +1,153 @@
using System.Runtime.Intrinsics.Arm;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using Microsoft.Win32.SafeHandles;
namespace ln.podget;
public class PodcastCatcher : IDisposable
{
public string LibraryPath { get; }
private HashSet<Uri> _feedList = new HashSet<Uri>();
public IEnumerable<Uri> Feeds => _feedList;
public PodcastCatcher(string libraryPath)
{
LibraryPath = Path.GetFullPath(libraryPath);
if (!Directory.Exists(LibraryPath))
throw new DirectoryNotFoundException(LibraryPath);
string feedsFile = $"{LibraryPath}/feeds.txt";
if (File.Exists(feedsFile))
AddFeeds(File.ReadLines(feedsFile));
}
public PodcastCatcher AddFeeds(IEnumerable<string> feedUris) =>
AddFeeds(feedUris.Where(u => u.Trim().Length > 0).Select(u => new Uri(u)));
public PodcastCatcher AddFeeds(IEnumerable<Uri> feedUris)
{
foreach (var feedUri in feedUris)
AddFeed(feedUri);
return this;
}
public PodcastCatcher AddFeed(Uri feedUri)
{
_feedList.Add(feedUri);
return this;
}
public void Catch()
{
foreach (var uri in _feedList)
{
new Thread(() => CatchFeed(uri)).Start();
}
}
public void CatchFeed(Uri feedUri)
{
var feed = new XMLFeed(feedUri);
foreach (var channel in feed.Channels)
{
Console.WriteLine(channel);
string channelPath = $"{LibraryPath}/{PathHelper.GetValidPathElement(LibraryPath, channel.Title)}";
PathHelper.EnsureDirectory(channelPath);
XmlDocument albumDocument = new XmlDocument();
XmlNode albumNode = albumDocument.CreateElement("album");
XmlNode titleNode = albumDocument.CreateElement("title");
titleNode.InnerText = channel.Title;
albumDocument.AppendChild(albumNode);
albumNode.AppendChild(titleNode);
XmlNode plotNode = albumDocument.CreateElement("plot");
plotNode.InnerText = channel.Description;
albumNode.AppendChild(plotNode);
if (channel.Thumb is not null)
{
XmlNode artNode = albumDocument.CreateElement("art");
XmlNode posterNode = albumDocument.CreateElement("poster");
posterNode.InnerText = channel.Thumb.ToString();
artNode.AppendChild(posterNode);
albumNode.AppendChild(artNode);
}
albumDocument.Save($"{channelPath}/album.nfo");
HashSet<string> itemIds = new HashSet<string>();
var idFile = $"{channelPath}/items.lst";
if (File.Exists(idFile))
foreach (var id in File.ReadLines(idFile))
itemIds.Add(id);
foreach (var channelItem in channel.Items)
{
Console.WriteLine(" " + channelItem);
byte[] guidBytes = Encoding.UTF8.GetBytes(channelItem.Guid);
SHA256 sha256 = SHA256.Create();
sha256.TransformFinalBlock(guidBytes, 0, guidBytes.Length);
var idString = GetHexString(sha256.Hash);
itemIds.Add(idString);
string itemPath =
$"{channelPath}/{idString}.{PathHelper.GetTypeExtension(channelItem.ContentType)}";
if (!File.Exists(itemPath))
{
using (var stream = channelItem.GetContent())
{
if (stream is not null)
{
using (FileStream fs = new FileStream(itemPath, FileMode.CreateNew))
stream.CopyTo(fs);
}
}
File.SetCreationTime(itemPath, channelItem.PubDate);
var tagFile = TagLib.File.Create(itemPath);
if ((tagFile.Tag.Title?.Length ?? 0) < channelItem.Title.Length)
tagFile.Tag.Title = channelItem.Title;
if ((tagFile.Tag.Description?.Length ?? 0) < channelItem.Description.Length)
tagFile.Tag.Description = channelItem.Description;
tagFile.Tag.Album = channel.Title;
if (tagFile.Tag.Track == 0)
tagFile.Tag.Track = (uint)itemIds.Count;
tagFile.Save();
}
}
File.WriteAllLines(idFile, itemIds.ToArray());
}
}
public void SaveFeeds()
{
string feedsFile = $"{LibraryPath}/feeds.txt";
File.WriteAllLines(feedsFile, _feedList.Select(uri => uri.ToString()));
}
public void Dispose()
{
SaveFeeds();
}
private string GetHexString(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
foreach (var b in bytes)
sb.Append($"{b:x2}");
return sb.ToString();
}
}

View File

@ -0,0 +1,20 @@
// See https://aka.ms/new-console-template for more information
using System.Text.RegularExpressions;
using System.Xml;
using ln.podget;
string libraryPath = args[0];
using (PodcastCatcher catcher = new PodcastCatcher(libraryPath))
{
if (args.Length > 1)
{
catcher.AddFeeds(args[1..]);
catcher.SaveFeeds();
}
else
{
catcher.Catch();
}
}

View File

@ -0,0 +1,43 @@
using System.Xml;
namespace ln.podget;
public class RSSChannel
{
public string Title { get; set; } = "";
public Uri? Link { get; set; }
public string Description { get; set; }
public string Language { get; set; }
public string Copyright { get; set; }
private SortedList<long, RSSItem> _items = new SortedList<long, RSSItem>();
public IEnumerable<RSSItem> Items => _items.Values;
public Uri Thumb { get; set; }
public RSSChannel(XmlNode channelNode)
{
Title = channelNode?.SelectSingleNode("title")?.InnerText ?? "";
var linkNode = channelNode?.SelectSingleNode("link");
Link = linkNode != null ? new Uri(linkNode.InnerText) : null;
Description = channelNode.SelectSingleNode("description")?.InnerText ?? "";
Language = channelNode.SelectSingleNode("language")?.InnerText ?? "";
Copyright = channelNode.SelectSingleNode("copyright")?.InnerText ?? "";
XmlNode thumbNode = channelNode.SelectSingleNode("image/url");
if (thumbNode is not null)
Thumb = new Uri(thumbNode.InnerText);
foreach (XmlNode itemNode in channelNode.SelectNodes("item"))
{
var i = new RSSItem(itemNode);
_items.Add(i.PubDate.ToFileTime(), i);
}
}
public override string ToString()
{
return $"{Title} / {Link}";
}
}

View File

@ -0,0 +1,58 @@
using System.Net;
using System.Xml;
namespace ln.podget;
public class RSSItem
{
public string Title { get; set; } = "";
public Uri? Link { get; set; }
public Uri? Enclosure { get; set; }
public string ContentType { get; set; }
public string Description { get; set; }
public DateTime PubDate { get; set; }
public string Author { get; set; }
public String Guid { get; set; }
public RSSItem(XmlNode itemNode)
{
Title = itemNode?.SelectSingleNode("title")?.InnerText ?? "";
var linkNode = itemNode?.SelectSingleNode("link");
Link = linkNode != null ? new Uri(linkNode.InnerText) : null;
var enclosureNode = itemNode?.SelectSingleNode("enclosure");
Enclosure = enclosureNode != null ? new Uri(enclosureNode.Attributes["url"].Value) : null;
ContentType = enclosureNode.Attributes["type"].Value;
Description = itemNode.SelectSingleNode("description")?.InnerText ?? "";
Author = itemNode.SelectSingleNode("author")?.InnerText ?? "";
Guid = itemNode.SelectSingleNode("guid")?.InnerText;
PubDate = DateTime.Parse(itemNode.SelectSingleNode("pubDate").InnerText);
}
public Stream? GetContent()
{
HttpClient httpClient = new HttpClient();
var taskGet = httpClient.GetAsync(Enclosure);
if (!taskGet.Wait(TimeSpan.FromMinutes(5)))
throw new TimeoutException();
var response = taskGet.Result;
if (response.StatusCode != HttpStatusCode.OK)
return null;
return response.Content.ReadAsStream();
}
public string GetFileName()
{
return PubDate.ToString("yyyy-MM-dd") + " " + PathHelper.GetValidPathElement(Title) + "." +
PathHelper.GetTypeExtension(ContentType);
}
public override string ToString()
{
return $"{PubDate.ToString("yyyy-MM-dd")} {Title} ({Link})";
}
}

View File

@ -0,0 +1,52 @@
using System.Net;
using System.Security.Cryptography;
using System.Xml;
namespace ln.podget;
public class XMLFeed
{
public static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
public Uri Uri { get; }
XmlDocument _XmlDocument;
private List<RSSChannel> _channels = new List<RSSChannel>();
public IEnumerable<RSSChannel> Channels => _channels;
public XMLFeed(Uri uri)
{
Uri = uri;
HttpClient httpClient = new HttpClient();
var taskGet = httpClient.GetAsync(uri);
if (!taskGet.Wait(DefaultTimeout))
throw new TimeoutException();
if (taskGet.Result.StatusCode != HttpStatusCode.OK)
throw new Exception(taskGet.Result.ReasonPhrase);
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.Load(taskGet.Result.Content.ReadAsStream());
Initialize(xmlDocument);
}
public XMLFeed(string filename)
{
XmlDocument document = new XmlDocument();
document.Load(filename);
Initialize(document);
}
public XMLFeed(XmlDocument document)
{
Initialize(document);
}
private void Initialize(XmlDocument xmlDocument)
{
_XmlDocument = xmlDocument;
foreach (XmlNode channelNode in _XmlDocument.SelectNodes("/rss/channel")!)
_channels.Add(new RSSChannel(channelNode));
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Update="examples\kruppstahl">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>
</Project>