So I decided to expand a little one-off C# console program I wrote awhile ago to download the free e-book offered by Syncfusion, and decided to also do it for other free e-book publishers... like O'Reilly.

But I now want to make it a generic e-book downloader, and make it easily expandable and maintainable. I plan to open source it on Github, and accept pull requests for any additional free e-book sites.

So currently, there's a main program that prints a menu, and depending on the chosen site, uses a specific "downloader class" to download the html of the page that lists the free e-books, and then uses regex (Yes I know now [thanks to this answer](https://codereview.stackexchange.com/a/156355/39601) that this is not the proper tool, I'm open to advise on C# html parsers) to create a list of *direct links* to those ebooks, attempt download them, and log any download errors.

**I would like input on how to best follow and implement SOLID principles for the downloader classes.**

So far I have:
An interface to define the most basic behavior/functionality

```
public interface IEbookDownloader
{
    void Run();
    void OutputLogs();
}

```

A base downloader class that implements it

```
public class BaseDownloader : IEbookDownloader
{
    public Uri EbooksUrl { get; private set; }

    public Uri DownloadFolder { get; private set; }

    public bool OvewriteExisting { get; private set; }

    public List<string> EbookDownloadUrls { get; set; }

    public List<string> BadEbookDownloadUrls { get; set; }

    /// <summary>
    /// Instantiates the BaseDownloader class
    /// </summary>
    /// <param name="downloadFolder">The string path where the file should be downloaded to.</param>
    /// <param name="overwrite">Flag that if true forces the downloader ignore existing/found downloaded files in the download folder, and re-download them.</param>
    public BaseDownloader(string ebooksurl, string downloadFolder, bool overwrite = false)
    {
        this.DownloadFolder = Uri.IsWellFormedUriString(downloadFolder, UriKind.RelativeOrAbsolute) ? new Uri(downloadFolder) : null;
        this.OvewriteExisting = overwrite;

        this.EbooksUrl = Uri.IsWellFormedUriString(ebooksurl, UriKind.Absolute) ? new Uri(ebooksurl) : null;

    }

    public void OutputLogs()
    {
        File.WriteAllLines(Path.Combine(this.DownloadFolder.ToString(), "links.txt"), this.EbookDownloadUrls);
        File.WriteAllLines(Path.Combine(this.DownloadFolder.ToString(), "404s.txt"), this.BadEbookDownloadUrls);
    }

    protected void PrepareToDownload()
    {
        // check data make sure we have proper dl destination and dl source url
        if (this.DownloadFolder == null || !Directory.CreateDirectory(this.DownloadFolder.ToString()).Exists)
        {
            throw new DirectoryNotFoundException("There was a problem with the downloads destination folder.");
        }

        if (this.EbooksUrl == null)
        {
            throw new Exception("The url passed is malformed.");
        }

        // Download html page with list of e-books
        var ebookspagehtmlpath = Path.Combine(this.DownloadFolder.ToString(), "ebookspage.html");
        if (!File.Exists(ebookspagehtmlpath) || this.OvewriteExisting)
        {
            using (var wclient = new WebClient())
            {
                wclient.DownloadFile(this.EbooksUrl, ebookspagehtmlpath);
            }
        }

        var pagehtml = File.ReadAllText(ebookspagehtmlpath).Replace("\"", "'");

        // TODO: For this generic one just web-scrape parse/regex html for urls that have: href='anything.[epub|pdf|mobi|zip]'
        // and fill in the BookDownloadUrls list, and the BadBookDownloadUrls
        var matches = Regex.Matches(pagehtml, @"href='(.+\.[epub|pdf|mobi|zip])'", RegexOptions.IgnoreCase);
        if (matches.Count > 0)
        {
            foreach (Match matchgroup in matches)
            {
                string titleslug = string.Empty;
                var cg = matchgroup.Groups[1].Value;
                if (Uri.IsWellFormedUriString(cg, UriKind.Absolute))
                {
                    Console.WriteLine("Will download {0}.", cg);
                    this.EbookDownloadUrls.Add(cg);
                }
            }
        }
    }

    public void Run()
    {
        this.PrepareToDownload();

        // thingy needed for TitleCasing Ebook folder names
        var textinfo = new CultureInfo("en-US", false).TextInfo;

        using (var wclient = new WebClient())
        {
            var ok200count = 0;
            foreach (var link in this.EbookDownloadUrls)
            {
                var urilink = new Uri(link);
                var filename = urilink.Segments.Last();

                var dotindex = filename.LastIndexOf(".");
                var booktitle = textinfo.ToTitleCase(filename.Substring(0, dotindex)).Replace(" ", "_");

                var ebookdir = Path.Combine(this.DownloadFolder.ToString(), booktitle);
                if (!Directory.Exists(ebookdir))
                {
                    Directory.CreateDirectory(ebookdir);
                }

                try // download of ebook
                {
                    var ebookfilepath = Path.Combine(ebookdir, filename);
                    if (!File.Exists(ebookfilepath) || this.OvewriteExisting)
                    {
                        wclient.DownloadFile(link, ebookfilepath);
                    }
                    else
                    {
                        Console.WriteLine("Skipped: File already in {0}", ebookfilepath);
                    }

                    Console.WriteLine(++ok200count + " files Downloaded");
                }
                catch (WebException e)
                {
                    Console.WriteLine("Error! Could not find {0} at {1}", booktitle, link);
                    Console.Write("Client error was: {0}", e.Message);
                    this.BadEbookDownloadUrls.Add(link);
                    continue;
                }
            }

            File.WriteAllLines(Path.Combine(this.DownloadFolder.ToString(), "links.txt"), this.EbookDownloadUrls);
            File.WriteAllLines(Path.Combine(this.DownloadFolder.ToString(), "notfound.txt"), this.BadEbookDownloadUrls);
        }
    }
}

```

And the original syncfusion downloader class:

```
using System;

public class SyncfusionDownloader : BaseDownloader
{
    public SyncfusionDownloader(string ebooksurl, string downloadFolder, bool overwrite = false)
        : base(ebooksurl, downloadFolder, overwrite)
    {
    }

    public override void Run()
    {
        // specific steps for Syncfusion's site
    }

}

```