Initial Commit
commit
7beedced84
|
@ -0,0 +1,5 @@
|
|||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
|
@ -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
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
|
@ -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>
|
|
@ -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
|
|
@ -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" },
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
|
@ -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})";
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
Loading…
Reference in New Issue