A personal hobby of mine is collecting CDs and whenever I acquire a new CD, there is an entire workflow that happens: The CD gets ripped, the cover art is acquired or created, then the files are processed to embed the cover art, lyrics, and ReplayGain values. These last three bits are something for which there is no decent tooling. So, I wrote a tool myself.
The most volatile part is finding and downloading lyrics for the songs. There is a gray area with regard to lyrics as they are usually included in the booklet “by permission” and are prone to litigation when published. As such, the availability of lyrics and the websites that host them are unpredictable.
Legality concerns aside, the utility I wrote has to cope with these disparate websites that come and go and also the differences between them. The original version of the lyric utility had a different class for each website, with specific logic as to how to download and parse the lyrics when given an artist and song title. Pretty general architecture. I had radio buttons in the application where you could select which site to attempt to download the lyrics from.
Over time, things got a little annoying. A site would vanish and I’d have to swap it out for another. Write a new class and edit the form to change the radio buttons, then change the code to instantiate the right class to process the lyrics. I should point out that early on, I implemented an interface so I only had to manage the instantiation of the right class. More typical architecture, growing as the the application’s requirements grow.
Finally, I was down to one working website and was ready to add a bunch more, but I was irritated at the thought of having to modify my UI code to support these new classes. I decided to make the classes external to the application using a plug-in type design. The interfaces and generic classes I was already using, I made them public, then created additional projects that referenced the main application and those classes. In the UI, I changed the radio buttons to a dropdown and loaded it up with instances of classes loaded from DLLs in the bin folder (I’m speaking very generally, here). Then I moved all my website-specific code into these other class projects and had the post-build action copy the DLL to the main application’s bin folder. The conversion only took an hour or so. The rest of the time was creating new parsers for new websites.
The meat of the code is simply looping through the DLLs (which are named consistently) and looping through the classes in each DLL looking for the classes that implement the interface.
lyricProviders = New Dictionary(Of String, ILyricProvider)
For Each path As String In IO.Directory.GetFiles(My.Application.Info.DirectoryPath, “*lyricprovider.dll”)
a = System.Reflection.Assembly.LoadFile(path)
For Each t As Type In a.GetTypes
If t.GetInterfaces.Contains(GetType(ILyricProvider)) Then
p = CType(a.CreateInstance(t.FullName), ILyricProvider)
So this Dictionary has a list of websites and an instance of the class that downloads the information. I look up a dictionary entry based on the name that was chosen the drop down list. I get an instance to execute to get the lyrics. Plug-ins utilizing interfaces are not as difficult as they may appear.
Maybe you’re thinking I’ve created more work for myself because if I want to add support for a new website, I have to create a whole new project to add it to the application. But now, I don’t have to touch any of the code in the main application ever again. I code to the interfaces and that’s it. If a website dies and I need to no longer support it, I delete its respective DLL from the bin folder and I’m done. No code changes at all.