Tijdens de presentatie van Scott Hanselman die ik bezocht heb (link) benoemde hij de podcasts op zijn website. Dagelijks rij ik een half uurtje heen naar mijn werk en een half uurtje terug, dus in plaats van naar de radio of muziek te luisteren, zou ik deze MP3's ook op CD kunnen branden. Op de overzichtspagina staan alle podcasts, waar je op kunt doorklikken. Op die pagina staan links naar iTunes, Spotify, maar ook een gewone download-link. Dit ga ik natuurlijk niet zelf allemaal aanklikken.
Open Visual Studio (Community Edition is vrij te gebruiken), maak een nieuwe console-applicatie en voeg bij de Nuget-packages "HtmlAgilityPack" toe. Hierna kun je met onderstaande code de links opvragen van de overzichtspagina en deze worden vervolgens verwerkt met het downloaden van de MP3 op de detailpagina.
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Xml;
using System.Xml.XPath;namespace ScottMinutesDownloader
{
class Program
{const string baseUrl = "https://hanselminutes.com";
const string saveLocation = @"c:\hanselminutes\mp3\";
static string logFileLocation = Path.Combine(new string[] { $"{saveLocation}", "downloadLog.txt" });
const int maxDownloadsPerSession = 30;
static List<string> downloadLog = new List<string>();static void Main(string[] args)
{
try
{
ReadLogFile();string url = $"{baseUrl}/archives";
int downloadResultCount = 0;
var nodesOverviewPage = GetHtmlNodes(url, "//a[@class=\"showCard\"]");
Stack<string> DownloadLinks = new Stack<string>();
while (nodesOverviewPage.MoveNext())
{
string detailUrl = "";
if (nodesOverviewPage.Current.MoveToAttribute("href", ""))
{
detailUrl = nodesOverviewPage.Current.Value;
}
if (string.IsNullOrEmpty(detailUrl) == false)
{
DownloadLinks.Push(detailUrl);
}
}
while (DownloadLinks.Count > 0)
{
string downloadLink = DownloadLinks.Pop();
if (DownloadMp3(downloadLink))
{
downloadResultCount++;
if (downloadResultCount >= maxDownloadsPerSession)
{
break;
}
}
}
}
catch (Exception x)
{
Console.WriteLine(x.ToString());
}
Console.WriteLine("done!");
Console.ReadKey();
}static string GetSaveFileName(string relativeUrl)
{
string result = "";
if (string.IsNullOrEmpty(relativeUrl) == false)
{
string[] urlParts = relativeUrl.Split(new char[] { '/' });
urlParts[1] = urlParts[1].PadLeft((4 - urlParts[0].Length < 0 ? 0 : 4 - urlParts[0].Length), '0');
StringBuilder saveFileName = new StringBuilder();
foreach (string s in urlParts)
{
saveFileName.Append($"{s}-");
}result = saveFileName.ToString().TrimStart(new char[] { '-' }).TrimEnd(new char[] { '-' }) + ".mp3";
}
return result;
}static bool PreviousDownloaded(string saveFileName)
{
return ((File.Exists(Path.Combine(new string[] { saveLocation, saveFileName }))) || (downloadLog.Contains(saveFileName)));
}static bool DownloadMp3(string relativeUrl)
{
bool result = false;
string saveFileName = GetSaveFileName(relativeUrl);
if (PreviousDownloaded(saveFileName))
{
return false;
}var nodesDetailPage = GetHtmlNodes($"{baseUrl}{relativeUrl}", @"//a[@download]");
while (nodesDetailPage.MoveNext())
{
string downloadUrl = "";
if (nodesDetailPage.Current.MoveToAttribute("href", ""))
{
downloadUrl = nodesDetailPage.Current.Value;
}
if (string.IsNullOrEmpty(downloadUrl) == false)
{
string saveFileLocation = Path.Combine(new string[] { saveLocation, saveFileName });
if ((File.Exists(saveFileLocation) == false)&&(downloadLog.Contains(saveFileLocation) == false))
{
Console.WriteLine($"Downloading {downloadUrl}");
using (WebClient wc = new WebClient())
{
File.WriteAllBytes(saveFileLocation, wc.DownloadData(downloadUrl));
}
AddDownloadToLogFile(saveFileName);
result = true;
}
}
}
return result;
}private static XPathNodeIterator GetHtmlNodes(string url, string pattern)
{
XPathNodeIterator result = null;
HtmlWeb htmlWeb = new HtmlWeb();
using (MemoryStream ms = new MemoryStream())
{
System.Xml.XmlTextWriter writer = new XmlTextWriter(ms, Encoding.UTF8);
htmlWeb.LoadHtmlAsXml(url, writer);
ms.Position = 0;
XPathDocument document = new XPathDocument(ms);
writer.Close();result = document.CreateNavigator().Select(pattern);
}
return result;
}private static void ReadLogFile()
{
if (File.Exists(logFileLocation))
{
downloadLog = File.ReadAllLines(logFileLocation).ToList<string>();
}
}private static void AddDownloadToLogFile(string downloadedFileName)
{
File.AppendAllText(logFileLocation, $"{downloadedFileName}\r\n");
}}
}
De Stack gebruik ik omdat de nieuwste aflevering bovenaan staat, maar ik wil met de oudste beginnen. En je ziet de File.Exists-controle, zodat als je het bestand al een keer gedownload hebt, de download-actie niet nogmaals uitgevoerd wordt. Ook schrijf ik de downloads (later bedacht) weg in een tekstbestand, mocht ik het MP3 bestand uit die map weghalen, dan is via het logbestand te zien dat het bestand al eerder gedownload is en niet nogmaals gedownload hoeft te worden. Omdat de URL al een nette weergave is van waar de aflevering over gaat én daar ook het volgnummer in zit (/25/talk-about-sql-server) gebruik ik dat om de MP3 een duidelijke naam te geven (het bestand wat we downloaden heeft geen representatieve naam, dat is iets als 384762.mp3). Het volgnummer vul ik op met nullen zodat er altijd 4 cijfers zijn aan het begin van de bestandsnaam en je dus kunt sorteren.
Zo kun je deze executable regelmatig draaien om je gedownloade afleveringen up-to-date te houden.