Intro

This blog post details my first experience of writing F# code. I've been interested in learning F# for about a year now, however never really got round to it as I don't get to use it in my day-job. I even attended a F# Meetup in Coventry, at the start of the year, before 2020 took a nose-dive.

A few months ago, I got the email that so many had received, GoogleMusic was shutting down and users were advised to migrate over to YouTube music. I have uploaded over 8,000 songs in GoogleMusic, love the minimalist interface and use it almost daily. I was a bit gutted as it's a piece of software I have used for years and years, works great on my Android phone and it just gets the job done.

I did as I was told and clicked the migrate button, to transfer all the songs to YouTube Music. After the wait, the first impressions weren't great. YouTube Music is terrible, I'm not going to spend the whole post ripping into it, but the separation between music I own (uploaded) and YouTube Content isn't well defined, you go and find a song you want to play, get there and realise it's playing from the YouTube Content database rather than your own copy - so you get some nice ads. Also the whole experience feels half-baked, more-so on the "personal library" side of things. Almost like they're trying to discourage you from uploading your own copies of music, and instead buying/listening from their ad-supported media.

(I've heard of FunkWhale and have began researching that to see if that'll fit my needs - planning to really give it a go in 2021!)

Anyway, back to how YouTube Music forced me into F#...

Hating the YouTube Music experience I immediately started looking for ways to download my entire library. I do have the original copies of all my tracks, but they're on an old HDD in the loft of my parent's house - so for now, just wanted something to work with.After GoogleMusic had been retired it's not possible to download all the tracks you had stored.......bummer!

However, I stumbled across an article which suggested using Google Takeout to download my music library. Google Takeout is a service where Google will make it easy to download all your data that they've got. How nice of them...!

Right, I started the download of 80GB of music and waited. Opened up the `.zip ` files to find that it's a totally flat file structure, the tidy folder structure the songs sat in was no more. Used to be Artist -> Album -> *tracks*.

I was seconds away from creating a new C# (netcore) console app and fixing this. Would have taken less than 15 mins and I would have my music all nicely sorted again. Great....but I wouldn't have learned anything and I've been wanting to try F# for a while. So, I gave it a go in F# - took a lot longer than 15 mins, maybe a few hours, on and off. But it works and sorts all 8,000 songs pretty quickly and does a good job too!

The Application

The Application itself is pretty simple, an absolute novice with F# like me can read this and get the general idea of what's going on. It's also really small too, so small in fact that it's embedded below. I've done my best at commenting it inline to show what's going on.

Note: This is my first attempt at F#, it's likely non-optimal and not representative of what proper F# devs would write. Please do let me know how it can be improved. I'm on Twitter @ryan_southgate

open System.IO
open Id3 // NuGet package reference (for mp3 ID3 tags): ID3

type GoogleMusicSong = {
    path: string
    artist: string
    album: string
}

/// Where the Sorted Songs will end up
let outputFolder = @"C:\Temp\GoogleMusicSorter\"

/// Invalid characters to remove from a directory path
let invalidChars = [| '*'; '!'; '.'; ':'; '?'; '"' |]

let getMp3Details (filename: string) = 
    let tag = (new Mp3(filename)).GetTag(Id3TagFamily.Version2X)
    let artist =
        match tag.Artists.Value.Count with
        | 0 -> "Unknown"
        | _ -> tag.Artists.Value.[0]
    { path = filename; artist = artist; album = tag.Album.Value };

/// Removes the invalid characters from the given string
let makeSafe = String.collect (fun c -> if Seq.exists((=)c) invalidChars then "" else string c)

/// Gets the "Safe" folder name for an Artist & Album
let getFolder folderName = 
    match folderName with
    | null | "" -> ""
    | _ -> makeSafe folderName

/// Gets the new Directory path for a Song
let getPath song = System.IO.Path.Combine(outputFolder, getFolder song.artist, getFolder song.album).ToString();

[<EntryPoint>]
let main argv =
    let rootFolder = argv.[0];

    let mp3sAndPaths = System.IO.Directory.GetFiles(rootFolder, "*.mp3") 
                       |> Array.map getMp3Details
                       |> Array.map (fun x -> (getPath x, x))

    for tp in mp3sAndPaths do 
        let (f, mp3) = tp
        
        Directory.CreateDirectory(f) |> ignore
        printf "Created Directory: %s \n" f

        let newFileLocation = match mp3.path.Split(".mp3").Length with
                                | 1 -> mp3.path
                                | _ -> mp3.path.Substring(0, mp3.path.IndexOf(".") + 4)

        let newFilePath = new FileInfo(newFileLocation)
        File.Copy(mp3.path, Path.Combine(f, newFilePath.Name))
        printf "Copied File: %s \n" mp3.path

    printf "Moved %i songs" mp3sAndPaths.Length

    0 // return an integer exit code


Below are a few thoughts on my experience with F#, what I liked and what I didn't. I'm primarily a C# Developer, so comparisons/observations below are with C#.

What I Liked

  • I refactored the code twice, without running it - and it worked flawlessly. I'd seldom do this with C#, I run quite regularly in my "dev-loop" and like to compile/run/hit breakpoints as I go.
  • it's only 60 lines, and is pretty neat, there's no "cruft" (like curly braces), no need for classes (in separate files), no "platform code" (static methods, hepler classes) almost, just code that actually does stuff (instead of Static classes with methods etc)
  • Type inference, it just worked, there's only 1 reference to "GoogleMusicSong" where as in C# there'd be a few (method params etc)
  • Putting it all in one file doesn't feel messy, I wouldn't leave this all in one file in C#, it would be split up into separate classes/files
  • Solution explorer is a waste of space now, there's one proj, with one file - for small console apps like this, I can minimise it
  • Match statements force you to account for all possibilities and makes sure I haven't forgot a case, C# (ifs / switch statements) are more lenient and assume I always know what I'm doing

What I Didn't Like

  • Intellisense (F12-ing) onto string.contains to see overloads etc doesn't work. This might be something wrong with my installation, but F12-ing onto other members works fine, just that this one doesn't
  • Lack of intellisense comments on .Net methods mp3/path.split when hovering over them I can't see any description or parameters descriptions. But works fine on Directory.CreateDirectory
  • My "dev loop" - I'm in the habit of hitting F5 to test code I've just written and add breakpoints at a place where I can inspect the values of new variables I've added. In hindsight I probably should have used F# interactive mode.

Conclusion

All in all I enjoyed the experience, I'm going to continue chipping away at F#. C# recently (to me), seems to be getting more and more "functional features", so getting a head-start on these is never a bad thing, also increasing exposure to Functional Programming will aid my thinking/reasoning when developing in OO (Object Oriented) programs too.

If you've got some improvements to the code above, or some advice (maybe a good book/online learning resource), then I'd love to hear about it and give it a go. Reach out to me on Twitter

Thanks for reading :)