Click here to Skip to main content
15,892,737 members
Articles / Programming Languages / C#

LyricsFetcher - The Easiest Way to Find Lyrics for your Songs

Rate me:
Please Sign up or sign in to vote.
4.93/5 (82 votes)
29 Oct 2009GPL325 min read 201.6K   2.4K   184  
An article describing the development of a non-trivial C#/.NET application to fetch lyrics for songs.
/*
 * This file handles all the functionality of the Lyrics tab of the main window
 *
 * Author: Phillip Piper
 * Date: 2009-03-28 16:49
 *
 * CHANGE LOG:
 * 2009-03-31  JPP  - Added MetaData button
 * 2009-03-28  JPP  Initial Version
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Web;
using System.Windows.Forms;

using BrightIdeasSoftware;

namespace LyricsFetcher
{
    partial class Form1
    {
        #region Initialization

        private void InitializeLyricsTab() {
            this.InitializeSongListView();
            this.InitializeSongDetails();
            this.InitializeSongLibrary();
            this.InitializeLyricsFetchAnimation();
            this.InitializeLyricsFetchManager();
        }

        /// <summary>
        /// Initialize the details portion of the UI
        /// </summary>
        private void InitializeSongDetails() {
            // Give each textbox a Munger that knows how to get and set the appropriate information
            this.textBoxTitle.Tag = new Munger("Title");
            this.textBoxArtist.Tag = new Munger("Artist");
            this.textBoxAlbum.Tag = new Munger("Album");
            this.textBoxGenre.Tag = new Munger("Genre");
            this.textBoxLyrics.Tag = new Munger("Lyrics");

            // Collect all the details boxes so we can handle them as a collection
            this.allDetailTextBoxes.Add(this.textBoxTitle);
            this.allDetailTextBoxes.Add(this.textBoxArtist);
            this.allDetailTextBoxes.Add(this.textBoxAlbum);
            this.allDetailTextBoxes.Add(this.textBoxGenre);
            this.allDetailTextBoxes.Add(this.textBoxLyrics);
        }
        private List<TextBox> allDetailTextBoxes = new List<TextBox>();

        private void InitializeSongListView() {
            // Create a typed wrapper to remove need to perform so many casts
            this.typedOlvSongs = new TypedObjectListView<Song>(this.olvSongs);

            // Generate aspect getters which are typically 3-5x faster than reflection
            this.typedOlvSongs.GenerateAspectGetters();

            this.olvColumnTitle.ImageGetter = delegate(object x) { return "music"; };
            this.typedOlvSongs.GetColumn(1).ImageGetter = delegate(Song song) {
                if (String.IsNullOrEmpty(song.Artist))
                    return -1;
                else
                    return "group";
            };

            // Show the current fetch status if there is one
            this.olvColumnLyricsStatus.AspectGetter = delegate(object x) {
                Song song = (Song)x;
                if (this.fetchManager.GetStatus(song) == LyricsFetchStatus.NotFound)
                    return song.LyricsStatusString;
                else
                    return this.fetchManager.GetStatusString(song);
            };

            byte[] olvState = this.Preferences.ListViewState;
            if (olvState != null)
                this.olvSongs.RestoreState(olvState);
        }

        private void InitializeLyricsFetchAnimation() {
            // Allow animations on the Lyrics Status column
            this.animationHelper = new AnimationHelper();
            this.animationHelper.Column = this.olvColumnLyricsStatus;
            this.animationHelper.SetCompositeAnimationImage(
                Properties.Resources.process_working, new Size(16, 16), 32);

            // The image should animate if it has any fetch status except notFound
            this.animationHelper.IsAnimatingGetter = delegate(object model) {
                Song song = (Song)model;
                return this.fetchManager.GetStatus(song) != LyricsFetchStatus.NotFound;
            };

            this.animationHelper.Start();
        }

        private void InitializeSongLibrary() {
            // Release any previous library
            if (this.library != null) {
                this.fetchManager.CancelAll();
                this.library.Close();
                this.library.ProgessEvent -= new EventHandler<ProgressEventArgs>(library_ProgessEvent);
                this.library.DoneEvent -= new EventHandler<ProgressEventArgs>(library_DoneEvent);
                this.library.PlayEvent -= new EventHandler<EventArgs>(library_PlayEvent);
                this.library.QuitEvent -= new EventHandler<EventArgs>(library_PlayEvent);
            }

            this.olvSongs.SetObjects(null);

            if (this.Preferences.LibraryApplication == LibraryApplication.ITunes) {
                this.library = new ITunesLibrary();
                this.Text = Properties.Resources.WindowLabelITunes;
            } else {
                this.library = new WmpLibrary();
                this.Text = Properties.Resources.WindowLabelWmp;
            }
            this.library.InitializeEvents();
            this.library.ProgessEvent += new EventHandler<ProgressEventArgs>(library_ProgessEvent);
            this.library.DoneEvent += new EventHandler<ProgressEventArgs>(library_DoneEvent);
            this.library.PlayEvent += new EventHandler<EventArgs>(library_PlayEvent);
            this.library.QuitEvent += new EventHandler<EventArgs>(library_QuitEvent);
            this.library.LoadSongs();

            this.UpdateDetails();
            this.EnableControls();
            this.UpdateStatusText();
        }

        #endregion

        #region UI Handling

        /// <summary>
        /// Enable or disable the controls to match the state of the UI
        /// </summary>
        private void EnableControlsLyrics() {
            bool hasSongs = (this.olvSongs.GetItemCount() > 0);
            bool hasSelection = (this.olvSongs.SelectedIndices.Count > 0);
            bool isSingleSelection = (this.olvSongs.SelectedIndices.Count == 1);

            // "Select..." buttons are enabled when there are songs in the listview
            this.buttonSelectMissing.Enabled = hasSongs;
            this.buttonSelectUntried.Enabled = hasSongs;
            this.buttonSelectAll.Enabled = hasSongs;
            this.buttonSelectNone.Enabled = hasSongs;

            // The Play button is only enabled when one song is selected.
            // It can then function as either a start or stop button.
            this.buttonPlay.Enabled = isSingleSelection;
            if (isSingleSelection && this.library.IsPlaying(this.olvSongs.SelectedObject as Song)) {
                this.buttonPlay.ImageKey = "stop";
                this.buttonPlay.Text = "Sto&p";
            } else {
                this.buttonPlay.ImageKey = "play";
                this.buttonPlay.Text = "&Play";
            }

            this.buttonSearch.Enabled = isSingleSelection && this.isNetworkAvailable;
            this.buttonFetch.Enabled = hasSelection && this.isNetworkAvailable;
            this.buttonMetaData.Enabled = hasSelection && this.isNetworkAvailable;
            this.buttonStop.Visible = this.library.IsLoading || this.fetchManager.IsFetching;

            // Disable commands that mess with the library while the library is loading
            this.toolStripMenuItemChooseLibrary.Enabled = !this.library.IsLoading;
            this.toolStripMenuItemReloadLibrary.Enabled = !this.library.IsLoading;
            this.toolStripMenuItemDiscardCache.Enabled = !this.library.IsLoading;
        }

        /// <summary>
        /// Update all the details with the common information from
        /// all the selected songs.
        /// </summary>
        private void UpdateDetails() {
            IList<Song> songs = this.typedOlvSongs.SelectedObjects;
            foreach (TextBox tb in this.allDetailTextBoxes) {
                tb.Enabled = songs.Count > 0;
                tb.Text = this.GetCommonValue(songs, (Munger)tb.Tag);
            }
        }

        private void UpdateStatusTextLyrics() {
            this.toolStripStatusLabel1.Text = String.Format("{0} selected. {1} in library.",
                this.olvSongs.SelectedIndices.Count, this.olvSongs.GetItemCount());

            if (this.fetchManager.CountFetching == 0 && this.fetchManager.CountWaiting == 0)
                this.labelFetchStatus.Text = "";
            else
                if (this.fetchManager.CountWaiting == 0)
                    this.labelFetchStatus.Text = String.Format("{0} fetching",
                        this.fetchManager.CountFetching);
                else
                    this.labelFetchStatus.Text = String.Format("{0} fetching\r\n{1} waiting",
                        this.fetchManager.CountFetching,
                        this.fetchManager.CountWaiting);
        }

        /// <summary>
        /// If the given munger returns the same value for all the given
        /// songs, then return that value. Otherwise, return an empty string.
        /// </summary>
        /// <param name="songs">The list of songs to be considered</param>
        /// <param name="munger">The munger which will extract the value</param>
        /// <returns>The common value or an empty string</returns>
        private string GetCommonValue(IList<Song> songs, Munger munger) {
            if (songs.Count == 0 || songs.Count > 1000)
                return "";

            string value = (string)munger.GetValue(songs[0]);
            for (int i = 1; i < songs.Count; i++) {
                if (value != (string)munger.GetValue(songs[i]))
                    return "";
            }

            return value;
        }

        /// <summary>
        /// The text in the given text box may have changed.
        /// If it has, update all selected songs with its new value.
        /// </summary>
        /// <param name="tb">The text box whose value has changed</param>
        private void CommitDetails(TextBox tb) {
            if (!tb.Modified)
                return;

            IList songs = this.olvSongs.SelectedObjects;
            if (songs.Count == 0)
                return;

            // Write the changed detail into the selected objects
            using (new WaitCursor()) {
                foreach (Song song in songs) {
                    try {
                        this.library.Cache.RemoveLyrics(song);
                        ((Munger)tb.Tag).PutValue(song, tb.Text.Trim());
                        song.Commit();
                        this.library.Cache.PutLyrics(song);
                    }
                    catch (COMException ex) {
                        if (this.ReportCommitFailed(song, ex))
                            break;
                    }
                }
                this.olvSongs.RefreshObjects(songs);
            }
            this.EnableControls();

            // Don't do this again until the user changes it another time
            tb.Modified = false;
        }

        /// <summary>
        /// Configure the text boxes of the details section to autocomplete
        /// their values. The Artist, Album and Genre text boxes will autocomplete.
        /// The Title and Lyrics will not.
        /// </summary>
        private void ConfigureAutoCompleteDetails() {
            int rowCount = this.olvSongs.GetItemCount();
            this.olvSongs.ConfigureAutoComplete(this.textBoxArtist, this.olvColumnArtist, rowCount);
            this.olvSongs.ConfigureAutoComplete(this.textBoxAlbum, this.olvColumnAlbum, rowCount);
            this.olvSongs.ConfigureAutoComplete(this.textBoxGenre, this.olvColumnGenre, rowCount);
        }

        #endregion

        #region Library and Fetch Event handlers
        // All of these events originate on non-UI threads, so we have to Invoke them

        void library_ProgessEvent(object sender, ProgressEventArgs e) {
            this.BeginInvoke(new MethodInvoker(delegate() {
                if (e.Percentage == 0) {
                    this.olvSongs.EmptyListMsg = "Initializing library...";
                    this.SetMetaDataListContents();
                } else
                    this.olvSongs.EmptyListMsg = String.Format("Loading: {0}%", e.Percentage);
            }));
        }

        void library_DoneEvent(object sender, ProgressEventArgs e) {
            this.BeginInvoke(new MethodInvoker(delegate() {
                if (e.IsCancelled)
                    this.olvSongs.EmptyListMsg = "Loading was cancelled";
                else
                    this.olvSongs.EmptyListMsg = "This library is empty";
                this.olvSongs.SetObjects(library.Songs);
                this.SetMetaDataListContents();
                this.ConfigureAutoCompleteDetails();
                this.EnableControls();
                this.UpdateStatusText();

                // Release the cache to save some resources
                this.library.Cache = null;
                GC.Collect();
            }));
        }

        void library_PlayEvent(object sender, EventArgs e) {
            this.BeginInvoke(new MethodInvoker(this.EnableControls));
        }

        void fetchManager_StatusEvent(object sender, LyricsFetchStatusEventArgs e) {
            this.BeginInvoke(new MethodInvoker(delegate() {
                this.olvSongs.RefreshObject(e.Song);
                this.EnableControls();
                this.UpdateStatusText();
                if (e.Status == LyricsFetchStatus.Done) {
                    this.library.CacheLyrics(e.Song);
                    if (this.olvSongs.IsSelected(e.Song))
                        this.UpdateDetails();
                }
            }));
        }

        void library_QuitEvent(object sender, EventArgs e) {
            this.BeginInvoke(new MethodInvoker(delegate() {
                if (this.preferences.LibraryApplication == LibraryApplication.ITunes) {
                    MessageBox.Show(this, Properties.Resources.ITunesClosedMsg, Properties.Resources.AppName);
                    this.olvSongs.SetObjects(null);
                    this.fetchManager.CancelAll();
                    this.UpdateDetails();
                    this.EnableControls();
                    this.UpdateStatusText();
                }
            }));
        }

        #endregion

        #region UI Event handlers

        private void buttonFetch_Click(object sender, EventArgs e) {
            foreach (Song song in this.olvSongs.SelectedObjects) {
                this.fetchManager.Queue(song);
            }
            this.EnableControls();
        }

        private void buttonSelectUntried_Click(object sender, EventArgs e) {
            using (new WaitCursor()) {
                this.olvSongs.SelectedObjects = this.library.UntriedSongs;
                this.EnsureSelectionVisible(this.olvSongs);
            };
        }

        private void buttonSelectMissing_Click(object sender, EventArgs e) {
            using (new WaitCursor()) {
                this.olvSongs.SelectedObjects = this.library.SongsWithoutLyrics;
                this.EnsureSelectionVisible(this.olvSongs);
            }
        }

        private void buttonSelectAll_Click(object sender, EventArgs e) {
            using (new WaitCursor()) {
                this.olvSongs.SelectAll();
            }
        }

        private void buttonSelectNone_Click(object sender, EventArgs e) {
            using (new WaitCursor()) {
                this.olvSongs.DeselectAll();
            }
        }

        private void buttonStop_Click(object sender, EventArgs e) {
            if (this.library.IsLoading)
                this.library.CancelLoad();
            else
                if (this.fetchManager.IsFetching)
                    this.fetchManager.CancelAll();

            this.EnableControls();
        }

        private void buttonMetaData_Click(object sender, EventArgs e) {
            IList songs = this.olvSongs.SelectedObjects;
            if (songs.Count == 0)
                return;

            foreach (Song song in songs) {
                this.metaDataFetchManager.Queue(song);
            }

            // Make sure the songs involved are visible
            if (!this.radioButtonShowAll.Checked) {
                this.olvMetaData.RemoveObjects(songs);
                this.olvMetaData.AddObjects(songs);
            }
            this.olvMetaData.SelectedObjects = songs;
            this.EnsureSelectionVisible(this.olvMetaData);
            this.EnableControls();
            this.UpdateMetaDataDetails();

            // Swap to the MetaData tab, unless the Shift key is down
            if ((Control.ModifierKeys & Keys.Shift) != Keys.Shift)
                this.tabControl1.SelectedIndex = 1;
        }

        private void buttonPlay_Click(object sender, EventArgs e) {
            this.PlaySong(this.olvSongs.SelectedObject as Song);
        }

        private void buttonSearch_Click(object sender, EventArgs e) {
            Song song = this.olvSongs.SelectedObject as Song;
            if (song == null)
                return;

            string url = String.Format(Properties.Settings.Default.SearchQuery,
                HttpUtility.UrlEncode(song.Title),
                HttpUtility.UrlEncode(song.Artist));

            // Why is Process.Start() in the Diagnostics??
            System.Diagnostics.Process.Start(url);
        }

        private void olvSongs_SelectionChanged(object sender, EventArgs e) {
            this.UpdateDetails();
            this.EnableControls();
            this.UpdateStatusText();
        }

        private void textBox_Validated(object sender, EventArgs e) {
            this.CommitDetails((TextBox)sender);
        }

        #endregion

        #region Private variables

        private LyricsFetchManager fetchManager = new LyricsFetchManager();
        private AnimationHelper animationHelper;
        private TypedObjectListView<Song> typedOlvSongs;

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Team Leader
Australia Australia
Phillip has been playing with computers since the Apple II was the hottest home computer available. He learned the fine art of C programming and Guru meditation on the Amiga.

C# and Python are his languages of choice. Smalltalk is his mentor for simplicity and beauty. C++ is to programming what drills are to visits to the dentist.

He worked for longer than he cares to remember as Lead Programmer and System Architect of the Objective document management system. (www.objective.com)

He has lived for 10 years in northern Mozambique, teaching in villages.

He has developed high volume trading software, low volume FX trading software, and is currently working for Atlassian on HipChat.

Comments and Discussions