You're going to be implementing a Music Library domain composed of 3 main models, Song, Artist, and Genre. The models will relate to each other and collaborate heavily. Additionally, you're going to be extracting some common functionality out of those models and into a module, Concerns::Findable, which you'll then mix back into the models. You'll then build a collaborating object, MusicImporter, that can parse a directory of MP3 files and use the extracted filenames to create instances of Song, Artist, and Genre objects. Finally, you'll build a CLI in bin/musiclibrary that is powered by a MusicLibraryController to provide a simple CLI that lets a user browse the library of MP3s imported by song, artist, and genre.
This is a complex lab with many parts, so go slowly. Take time to understand what you're building holistically before starting. Read this entire README before jumping in. As you go from spec to spec, we recommend doing them in numbered order.
The first thing to do is get the basics of the main models working. Each model has almost the exact same basic requirements, so once you make 001_song_basics_spec.rb pass by building the Song class, the basic Artist and Genre specs will go quickly.
The requirements for each model are that they can accept a name upon initialization and set that property correctly. The name property should be readable and writable by the object.
Song.new("Blank Space").name #=> "Blank Space"`Additionally, each class should contain a class variable @@all that is set to an empty array and is prepared to store all saved instances of the class. This class variable should be accessible via the class method .all.
Song.all #=> []
Artist.all #=> []Instances should respond to a #save method that adds the instance itself into the appropriate @@all class variable.
Song.new("Blank Space").save
Song.all #=> [#<Song: @name="Blank Space">]The class should be able to empty its @@all array via a class method .destroy_all.
Song.new("Kaohsiung Christmas").save
Song.all #=> [#<Song: @name="Blank Space">, #<Song: @name="Kaohsiung Christmas">]
Song.destroy_all
Song.all #=> []Finally, all classes should implement a custom constructor .create that instantiates an instance using .new but also invokes #save on that instance, forcing it to persist immediately.
Song.new("Blank Space")
Song.all #=> []
Song.create("Blank Space")
Song.all #=> [#<Song: @name="Blank Space">]- Songs belong to an artist and an artist has many songs. Adding a song to an artist is done by calling an
#add_songmethod on an instance of theArtistclass. - Songs can be initialized with an optional
artistargument.
- Genres have many songs and are initialized with an empty list of songs.
- Songs have one genre.
- Songs can be initialized with an optional
genreargument.
- Artists have many genres through their songs. Implement a
#genresmethod for this association. - Genres have many artists through their songs. Implement an
#artistsmethod for this association.
Note: there are a few tests concerned with switching the
Song#initializemethod from setting instance variables for@artistand@genreto using the custom setter methods that you define (e.g.,Song#genre=). We want to use the custom setter methods because they keep our associations in sync. For example, when we call our customSong#artist=method, it sets the song's@artistproperty and adds the song to the artist's collection of songs. When you reach these tests, make sure those setter methods are only invoked ifSong#initializeis called with artist and/or genre arguments. Otherwise, the@artistand/or@genreproperties will be initialized asnil, and you'll have some unexpected consequences in both your code and the test suite.
- If we call
Song.new("Song Title", artist_object, genre_object), bothSong#artist=andSong#genre=should be invoked. - If we call
Song.new("This Song Has No Artist or Genre"), neitherSong#artist=norSong#genre=should be invoked.
First implement the following two methods in your Song class:
- Songs should have a
find_by_namemethod. - Songs should have a
find_or_create_by_namemethod.
Now that you've gotten the methods working in Song, let's adapt them for general reuse by putting them into a module that we can mix into our Artist and Genre classes. It's Ruby convention to put modules in a concerns/ folder nested under lib/, and each module should be namespaced like this:
module Concerns::ModuleName
# Module code here
endOnce the basic module structure is good to go, it's time to code our two class methods again:
- Implement a generic
#find_by_namemethod that uses the.allmethod defined by the class to find an instance of the class by name. - Implement a generic
#find_or_create_by_namemethod that uses the.allmethod defined by the class to find an instance of the class by name and to create a new instance if a match is not found. - Add this module to your
GenreandArtistclass.
Create a MusicImporter class that works with your Song, Genre, and Artist objects to import a directory of MP3 files. This class will have the following methods:
#initializeaccepts a file path to a directory of MP3 files.#filesreturns all of the imported filenames..importimports all of the files from the library, instantiating a newSongobject for each file.
In addition, add the following pair of methods to your Song class:
.new_from_filename, which instantiates a newSongobject based on a provided filename..create_from_filename, which does the same thing as.new_from_filenamebut also saves the newly-created song to the@@allclass variable.
Congrats! You've done the heavy lifting. Now let's wrap it all up in a simple CLI so that users can actually interact with our code. Create a MusicLibraryController class that:
- Upon initialization, accepts an optional path to the library of MP3 files, defaulting to
./db/mp3s. It should then instantiate aMusicImporterobject, which it will use to import songs from the specified library. - Has a
#callmethod that starts the CLI and prompts the user for input (try usinggets.chomp). Read the tests carefully for specifics.
When you've passed all of the tests, you should be able to type ./bin/musiclibrary into your terminal to play around with your CLI and see how it works.
Have fun!
- QA with Students
- This Q&A led by Avi Flombaum covers setting up a bin file, setting up a
Gemfileand installing gems, and identifying objects and their responsibilities. It contains general tips on requirements, gems, and design.
- This Q&A led by Avi Flombaum covers setting up a bin file, setting up a
- Debugging an Error in Music Library CLI
- This walk-through traces an error to its source in the code. In addition it covers how to change file permissions and how to create an executable file to initialize a sandbox environment.
- Lab Review