├── .gitignore ├── .npmignore ├── .travis.yml ├── History.md ├── Makefile ├── README.md ├── circle.yml ├── examples ├── artistSearch.js ├── lyricsSearch.js └── songSearch.js ├── package.json ├── src ├── constants │ └── Constants.js ├── geniusClient.js ├── model │ ├── Artist.js │ ├── Lyrics.js │ └── Song.js ├── parsers │ ├── ArtistParser.js │ ├── LyricsParser.js │ └── SongsParser.js └── util │ └── StringUtils.js └── test ├── rapGeniusClientTest.js ├── rockGeniusClientTest.js └── unitTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | initproject.sh 4 | RapGenius-JS.iml -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | initproject.sh 4 | RapGenius-JS.iml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.1.2 / 2014-06-28 2 | ==================== 3 | * Fixed tests that were broken with new updated HTML code of RapGenius. 4 | 5 | 0.1.1 / 2014-04-12 6 | ==================== 7 | * Added examples in folder 8 | * Fixed bugs 9 | 10 | 0.1.0 / 2014-02-02 11 | ==================== 12 | * Non backwards compatible changes introduced: 13 | - you can now search rock and rap artists, songs, and annotations by passing the type parameter 14 | * Also performed some refactoring 15 | 16 | 17 | 0.0.7 / 2013-12-26 18 | ==================== 19 | * Adding extra fields to RapLyrics model object: 20 | - songTitle 21 | - mainArtist 22 | - featuringArtists 23 | - producingArtists 24 | 25 | 0.0.5 / 2013-01-27 26 | ==================== 27 | * Refactored RapLyrics model object 28 | * Added new feature to retrieve lyrics and their meaning 29 | 30 | 0.0.4 / 2013-01-27 31 | ==================== 32 | * Renaming main .js file from "genuisClient.js" to "geniusClient.js" 33 | 34 | 0.0.3 / 2013-01-26 35 | ==================== 36 | * Integrating with Travis and fixing all bugs that come with it... 37 | 38 | 0.0.2 / 2013-01-25 39 | ==================== 40 | * Release with more bug fixes 41 | * Also added unit tests 42 | 43 | 0.0.1 / 2013-01-18 44 | ==================== 45 | * Initial release -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | node test/unitTests.js 3 | node test/rapGeniusClientTest.js 4 | node test/rockGeniusClientTest.js 5 | 6 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RapGenius-JS [![Build Status](https://travis-ci.org/kenshiro-o/RapGenius-JS.png?branch=master)](https://travis-ci.org/kenshiro-o/RapGenius-JS) 2 | 3 | rapgenius-js is a simple client that enables you to query RapGenius(www.rapgenius.com) and retrieve 4 | information about rap and rock artists and songs. 5 | 6 | ## Rationale 7 | 8 | This project was created because RapGenius does currently not support a Node.js API. 9 | 10 | ## Installation 11 | 12 | ```bash 13 | $ npm install rapgenius-js 14 | ``` 15 | 16 | ## Usage 17 | 18 | The API is very simple to use and currently enables you to perform the following: 19 | 20 | ### Model objects 21 | 22 | #### Artist 23 | Artist 24 | - name: String 25 | - link: String 26 | - popularSongs: Array (of String) 27 | - songs: Array (of String) 28 | 29 | #### Song 30 | Song 31 | - name: String 32 | - artists: String 33 | - link: String 34 | 35 | #### Lyrics 36 | Verses 37 | - id: int 38 | - content: String 39 | - explanation: String 40 | 41 | Section 42 | - name: String 43 | - content: String 44 | - verses: Array (of Verses) 45 | 46 | Lyrics 47 | - songId: int 48 | - songTitle: String 49 | - mainArtist: String 50 | - featuringArtists: Array (of String) 51 | - producingArtists: Array (of String) 52 | - sections: Array (of Section) 53 | 54 | ### Search for an artist: 55 | 56 | ```js 57 | var rapgeniusClient = require("rapgenius-js"); 58 | 59 | rapgeniusClient.searchArtist("GZA", "rap", function(err, artist){ 60 | if(err){ 61 | console.log("Error: " + err); 62 | }else{ 63 | console.log("Rap artist found [name=%s, link=%s, popular-songs=%d]", 64 | artist.name, artist.link, artist.popularSongs.length); 65 | 66 | } 67 | }); 68 | 69 | //Example for a rock artist 70 | rapgeniusClient.searchArtist("Bruce Springsteen", "rock", function(err, artist){ 71 | if(err){ 72 | console.log("Error: " + err); 73 | }else{ 74 | console.log("Rap artist found [name=%s, link=%s, popular-songs=%d]", 75 | artist.name, artist.link, artist.popularSongs.length); 76 | 77 | } 78 | }); 79 | ``` 80 | 81 | ### Search for a song: 82 | 83 | ```js 84 | var rapgeniusClient = require("rapgenius-js"); 85 | 86 | rapgeniusClient.searchSong("Liquid Swords", "rap", function(err, songs){ 87 | if(err){ 88 | console.log("Error: " + err); 89 | }else{ 90 | console.log("Songs that matched the search query were found" + 91 | "[songs-found=%d, first-song-name=%s", songs.length, songs[0].name); 92 | } 93 | }); 94 | ``` 95 | 96 | ### Search for the lyrics of a song along with their meaning: 97 | 98 | ```js 99 | var rapgeniusClient = require("rapgenius-js"); 100 | 101 | var lyricsSearchCb = function(err, lyricsAndExplanations){ 102 | if(err){ 103 | console.log("Error: " + err); 104 | }else{ 105 | //Printing lyrics with section names 106 | var lyrics = lyricsAndExplanations.lyrics; 107 | var explanations = lyricsAndExplanations.explanations; 108 | console.log("Found lyrics for song [title=%s, main-artist=%s, featuring-artists=%s, producing-artists=%s]", 109 | lyrics.songTitle, lyrics.mainArtist, lyrics.featuringArtists, lyrics.producingArtists); 110 | console.log("**** LYRICS *****\n%s", lyrics.getFullLyrics(true)); 111 | 112 | //Now we can embed the explanations within the verses 113 | lyrics.addExplanations(explanations); 114 | var firstVerses = lyrics.sections[0].verses[0]; 115 | console.log("\nVerses:\n %s \n\n *** This means ***\n%s", firstVerses.content, firstVerses.explanation); 116 | } 117 | }; 118 | 119 | var searchCallback = function(err, songs){ 120 | if(err){ 121 | console.log("Error: " + err); 122 | }else{ 123 | if(songs.length > 0){ 124 | //We have some songs 125 | rapgeniusClient.searchLyricsAndExplanations(songs[0].link, "rap", lyricsSearchCb); 126 | } 127 | } 128 | }; 129 | 130 | rapgeniusClient.searchSong("Liquid Swords", "rap", searchCallback); 131 | ``` 132 | 133 | 134 | ## Additional features 135 | 136 | I will work on the following features when I get the time: 137 | - Refactor code base 138 | - Improve performance 139 | 140 | ## Licence 141 | 142 | MIT (Make It Tremendous) 143 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | timezone: Europe/London 3 | node: 4 | version: 6.1.0 5 | -------------------------------------------------------------------------------- /examples/artistSearch.js: -------------------------------------------------------------------------------- 1 | var rapgeniusClient = require("../src/geniusClient.js"); 2 | 3 | rapgeniusClient.searchArtist("GZA", "rap", function(err, artist){ 4 | if(err){ 5 | console.log("Error: " + err); 6 | }else{ 7 | console.log("Rap artist found [name=%s, link=%s, popular-songs=%d]", 8 | artist.name, artist.link, artist.popularSongs.length); 9 | 10 | } 11 | }); 12 | 13 | //Example for a rock artist 14 | rapgeniusClient.searchArtist("Bruce Springsteen", "rock", function(err, artist){ 15 | if(err){ 16 | console.log("Error: " + err); 17 | }else{ 18 | console.log("Rap artist found [name=%s, link=%s, popular-songs=%d]", 19 | artist.name, artist.link, artist.popularSongs.length); 20 | } 21 | }); -------------------------------------------------------------------------------- /examples/lyricsSearch.js: -------------------------------------------------------------------------------- 1 | var rapgeniusClient = require("../src/geniusClient"); 2 | 3 | var lyricsSearchCb = function(err, lyricsAndExplanations){ 4 | if(err){ 5 | console.log("Error: " + err); 6 | }else{ 7 | //Printing lyrics with section names 8 | var lyrics = lyricsAndExplanations.lyrics; 9 | var explanations = lyricsAndExplanations.explanations; 10 | console.log("Found lyrics for song [title=%s, main-artist=%s, featuring-artists=%s, producing-artists=%s]", 11 | lyrics.songTitle, lyrics.mainArtist, lyrics.featuringArtists, lyrics.producingArtists); 12 | console.log("**** LYRICS *****\n%s", lyrics.getFullLyrics(true)); 13 | 14 | //Now we can embed the explanations within the verses 15 | lyrics.addExplanations(explanations); 16 | var firstVerses = lyrics.sections[0].verses[0]; 17 | console.log("\nVerses:\n %s \n\n *** This means ***\n%s", firstVerses.content, firstVerses.explanation); 18 | } 19 | }; 20 | 21 | var searchCallback = function(err, songs){ 22 | if(err){ 23 | console.log("Error: " + err); 24 | }else{ 25 | if(songs.length > 0){ 26 | //We have some songs 27 | rapgeniusClient.searchLyricsAndExplanations(songs[0].link, "rap", lyricsSearchCb); 28 | } 29 | } 30 | }; 31 | 32 | rapgeniusClient.searchSong("Liquid Swords", "rap", searchCallback); -------------------------------------------------------------------------------- /examples/songSearch.js: -------------------------------------------------------------------------------- 1 | var rapgeniusClient = require("../src/geniusClient"); 2 | 3 | rapgeniusClient.searchSong("Liquid Swords", "rap", function(err, songs){ 4 | if(err){ 5 | console.log("Error: " + err); 6 | }else{ 7 | console.log("Songs that matched the search query were found" + 8 | "[songs-found=%d, first-song-name=%s]", songs.length, songs[0].name); 9 | } 10 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rapgenius-js", 3 | "description": "A client that queries the RapGenius (www.rapgenius.com) website", 4 | "version": "0.1.2", 5 | "keywords": ["rap", "rock", "rapgenius", "client", "lyrics", "music"], 6 | "author": "kenshiro-o", 7 | "main": "src/geniusClient", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/kenshiro-o/RapGenius-JS.git" 11 | }, 12 | "dependencies": { 13 | "superagent": "0.12.x", 14 | "cheerio": "0.10.x" 15 | }, 16 | "devDependencies": { 17 | "vows": "*", 18 | "mocha": "*" 19 | }, 20 | "scripts" : { 21 | "test" : "make test" 22 | }, 23 | "engines": { 24 | "node": "0.10.x" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/constants/Constants.js: -------------------------------------------------------------------------------- 1 | var Type2URLs = { 2 | "rock": { 3 | "artist_url":"http://rock.genius.com/artists/", 4 | "base_url":"http://rock.genius.com", 5 | "search_url":"http://rock.genius.com/search", 6 | "annotations_url": "http://rock.genius.com/annotations/for_song_page" 7 | }, 8 | "rap": { 9 | "artist_url":"http://genius.com/artists/", 10 | "base_url":"http://genius.com", 11 | "search_url":"http://genius.com/search", 12 | "annotations_url": "http://genius.com/annotations/for_song_page" 13 | } 14 | 15 | }; 16 | 17 | module.exports.Type2URLs = Type2URLs; 18 | 19 | module.exports.ROCK_RAP_GENIUS_URL = "http://rock.genius.com"; 20 | module.exports.ROCK_RAP_GENIUS_URL_SEARCH_URL = "http://rock.genius.com/search"; 21 | module.exports.ROCK_RAP_GENIUS_ARTIST_URL = "http://rock.genius.com/artists/"; 22 | 23 | module.exports.RAP_GENIUS_URL = "http://genius.com"; 24 | module.exports.RAP_GENIUS_URL_SEARCH_URL = "http://genius.com/search"; 25 | module.exports.RAP_GENIUS_ARTIST_URL = "http://genius.com/artists/"; 26 | -------------------------------------------------------------------------------- /src/geniusClient.js: -------------------------------------------------------------------------------- 1 | var superAgent = require("superagent"), 2 | RapSongParser = require("./parsers/SongsParser"), 3 | RapArtistParser = require("./parsers/ArtistParser"), 4 | RapLyricsParser = require("./parsers/LyricsParser") 5 | Constants = require("./constants/Constants"); 6 | 7 | var RAP_GENIUS_URL = "http://genius.com"; 8 | var RAP_GENIUS_ARTIST_URL = "http://genius.com/artists/"; 9 | var RAP_GENIUS_SONG_EXPLANATION_URL = RAP_GENIUS_URL + "/annotations/for_song_page"; 10 | 11 | 12 | function searchSong(query, type, callback) { 13 | //TODO perform input validation 14 | 15 | type = type.toLowerCase(); 16 | var type2Urls = Constants.Type2URLs[ type]; 17 | if (!type2Urls){ 18 | process.nextTick(function(){ 19 | callback("Unrecognized type in song search [type=" + type + "]"); 20 | }); 21 | return; 22 | } 23 | 24 | superAgent.get(type2Urls.search_url) 25 | .query({q: query}) 26 | .set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 27 | .end(function (res) { 28 | if (res.ok) { 29 | var result = RapSongParser.parseSongHTML(res.text, type); 30 | if (result instanceof Error) { 31 | return callback(result); 32 | } else { 33 | return callback(null, result) 34 | } 35 | } else { 36 | console.log("Received a non expected HTTP status [status=" + res.status + "]"); 37 | return callback(new Error("Unexpected HTTP status: " + res.status)); 38 | } 39 | }); 40 | } 41 | 42 | function searchArtist(artist, type, callback) { 43 | //TODO perform input validation 44 | type = type.toLowerCase(); 45 | var type2Urls = Constants.Type2URLs[ type]; 46 | if (!type2Urls){ 47 | process.nextTick(function(){ 48 | callback("Unrecognized type in artist search [type=" + type + "]"); 49 | }); 50 | return; 51 | } 52 | 53 | superAgent.get(type2Urls.artist_url + artist) 54 | .set("Accept", "text/html") 55 | .end(function (res) { 56 | debugger; 57 | if (res.ok) { 58 | var result = RapArtistParser.parseArtistHTML(res.text, type); 59 | if (result instanceof Error) { 60 | return callback(result); 61 | } else { 62 | return callback(null, result); 63 | } 64 | } else { 65 | console.log("Received a non expected HTTP status [status=" + res.status + "]"); 66 | return callback(new Error("Unexpected HTTP status: " + res.status)); 67 | } 68 | }); 69 | } 70 | 71 | 72 | function searchSongLyrics(link, type, callback){ 73 | //Check whether the URL is fully defined or relative 74 | type = type.toLowerCase(); 75 | var type2Urls = Constants.Type2URLs[ type]; 76 | if (!type2Urls){ 77 | process.nextTick(function(){ 78 | callback("Unrecognized type in song lyrics search [type=" + type + "]"); 79 | }); 80 | return; 81 | } 82 | 83 | var url = /^http/.test(link) ? link : type2Urls.base_url + link; 84 | superAgent.get(url) 85 | .set("Accept", "text/html") 86 | .end(function(res){ 87 | if(res.ok){ 88 | var result = RapLyricsParser.parseLyricsHTML(res.text, type); 89 | if(result instanceof Error){ 90 | return callback(result); 91 | }else{ 92 | return callback(null, result); 93 | } 94 | }else{ 95 | console.log("An error occurred while trying to access lyrics[url=%s, status=%s]", url, res.status); 96 | return callback(new Error("Unable to access the page for lyrics [url=" + link + "]")); 97 | } 98 | }); 99 | } 100 | 101 | function searchLyricsExplanation(songId, type, callback){ 102 | //Check whether the URL is fully defined or relative 103 | 104 | type = type.toLowerCase(); 105 | var type2Urls = Constants.Type2URLs[ type]; 106 | if (!type2Urls){ 107 | process.nextTick(function(){ 108 | callback("Unrecognized type in song lyrics search [type=" + type + "]"); 109 | }); 110 | return; 111 | } 112 | 113 | superAgent.get(type2Urls.annotations_url) 114 | .set("Accept", "text/html") 115 | .query({song_id: songId}) 116 | .end(function(res){ 117 | if(res.ok){ 118 | var explanations = RapLyricsParser.parseLyricsExplanationJSON(JSON.parse(res.text)); 119 | if(explanations instanceof Error){ 120 | return callback(explanations); 121 | }else{ 122 | return callback(null, explanations); 123 | } 124 | }else{ 125 | console.log("An error occurred while trying to get lyrics explanation[song-id=%s, status=%s]", songId, res.status); 126 | return callback(new Error("Unable to access the page for lyrics [url=" + songId + "]")); 127 | } 128 | }); 129 | } 130 | 131 | function searchLyricsAndExplanations(link, type, callback){ 132 | var lyrics = null; 133 | var lyricsCallback = function(err, rapLyrics){ 134 | if(err){ 135 | return callback(err); 136 | }else{ 137 | lyrics = rapLyrics; 138 | searchLyricsExplanation(lyrics.songId, type, explanationsCallback); 139 | } 140 | }; 141 | 142 | var explanationsCallback = function(err, explanations){ 143 | if(err){ 144 | return callback(err); 145 | }else{ 146 | return callback(null, {lyrics: lyrics, explanations: explanations}); 147 | } 148 | }; 149 | 150 | searchSongLyrics(link, type, lyricsCallback); 151 | } 152 | 153 | module.exports.searchSong = searchSong; 154 | module.exports.searchArtist = searchArtist; 155 | module.exports.searchSongLyrics = searchSongLyrics; 156 | module.exports.searchLyricsExplanation = searchLyricsExplanation; 157 | module.exports.searchLyricsAndExplanations = searchLyricsAndExplanations; 158 | -------------------------------------------------------------------------------- /src/model/Artist.js: -------------------------------------------------------------------------------- 1 | function Artist(name, link) { 2 | this.name = name; 3 | this.link = link; 4 | this.popularSongs = []; 5 | this.songs = []; 6 | } 7 | 8 | Artist.prototype = { 9 | name: "", 10 | link: "", 11 | popularSongs: null, 12 | songs: null 13 | }; 14 | 15 | Artist.prototype.addPopularSong = function (rapSong) { 16 | this.popularSongs.push(rapSong); 17 | }; 18 | 19 | Artist.prototype.addSong = function (rapSong) { 20 | this.songs.push(rapSong); 21 | }; 22 | 23 | 24 | module.exports = Artist; -------------------------------------------------------------------------------- /src/model/Lyrics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple model objects representing the lyrics 3 | */ 4 | 5 | 6 | /** 7 | * Verses are lines from a song 8 | */ 9 | function Verses(id){ 10 | this.id = id; 11 | } 12 | 13 | Verses.prototype = { 14 | id: -1, 15 | content: "", 16 | explanation: "" 17 | }; 18 | 19 | Verses.prototype.addContent = function(content){ 20 | this.content += content; 21 | }; 22 | 23 | 24 | module.exports.Verses = Verses; 25 | 26 | 27 | /** 28 | * A section comprises multiple verses 29 | */ 30 | function Section(name){ 31 | this.name = name; 32 | this.verses = []; 33 | } 34 | 35 | Section.prototype = { 36 | name: "", 37 | verses: null 38 | }; 39 | 40 | Section.prototype.addVerses = function(verses){ 41 | this.verses.push(verses); 42 | }; 43 | 44 | 45 | module.exports.Section = Section; 46 | 47 | 48 | /** 49 | * Lyrics represents a collection of sections, 50 | * which in turn contain a collection of verses 51 | */ 52 | function Lyrics(id){ 53 | this.songId = id; 54 | this.sections = []; 55 | } 56 | 57 | Lyrics.prototype = { 58 | songId: -1, 59 | songTitle: "", 60 | mainArtist: "", 61 | featuringArtists: [], 62 | producingArtists: [], 63 | sections: null 64 | }; 65 | 66 | Lyrics.prototype.addSection = function(section){ 67 | this.sections.push(section); 68 | }; 69 | 70 | Lyrics.prototype.getFullLyrics = function (withSectionNames){ 71 | var fullLyrics = ""; 72 | this.sections.forEach(function(section, index){ 73 | fullLyrics += ((withSectionNames) ? section.name + "\n" : "" ); 74 | section.verses.forEach(function(verses, index){ 75 | var separation = ""; 76 | //This is to make sure we don't have bits of lyrics that are stuck together 77 | //as opposed to being separated by a space 78 | if (/[A-Za-z0-9]/.test(fullLyrics.charAt(fullLyrics.length - 1))){ 79 | separation = " "; 80 | } 81 | fullLyrics += separation + verses.content; 82 | }); 83 | }); 84 | 85 | return fullLyrics; 86 | }; 87 | 88 | Lyrics.prototype.addExplanations = function(explanations){ 89 | this.sections.forEach(function(section){ 90 | section.verses.forEach(function(verse){ 91 | var explanation = explanations[verse.id]; 92 | if(explanation){ 93 | verse.explanation = explanation; 94 | } 95 | }); 96 | }); 97 | }; 98 | 99 | 100 | module.exports.Lyrics = Lyrics; -------------------------------------------------------------------------------- /src/model/Song.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple model class for a song 3 | */ 4 | 5 | function Song(name, artists, link) { 6 | this.name = name; 7 | this.artists = artists; 8 | this.link = link; 9 | } 10 | 11 | Song.prototype = { 12 | name: "", 13 | artists: "", 14 | link: "", 15 | lyrics: null 16 | } 17 | 18 | module.exports = Song; -------------------------------------------------------------------------------- /src/parsers/ArtistParser.js: -------------------------------------------------------------------------------- 1 | var cheerio = require("cheerio"), 2 | CONSTANTS = require("../constants/Constants"), 3 | Song = require("../model/Song"), 4 | Artist = require("../model/Artist"), 5 | StringUtils = require("../util/StringUtils"); 6 | 7 | 8 | function parseArtistHTML(html, type) { 9 | try { 10 | var urls = CONSTANTS.Type2URLs[type]; 11 | var $ = cheerio.load(html); 12 | 13 | var artistElem = $(".canonical_name", "#main"); 14 | var artistName = ""; 15 | 16 | if (artistElem.length <= 0) { 17 | return new Error("Could not find artist"); 18 | } 19 | 20 | //TODO either find a library that enables be to extract text from text nodes of direct 21 | // children or improve cheerio API 22 | artistElem = artistElem[0]; 23 | 24 | artistElem.children.forEach(function (childElem) { 25 | if (childElem.type === "text") { 26 | artistName += StringUtils.removeWhiteSpacesAndNewLines(childElem.data); 27 | } 28 | }); 29 | 30 | var artistLink = urls.artist_url + artistName.replace(" ", "-"); 31 | var rapArtist = new Artist(artistName, artistLink); 32 | 33 | var songs = $(".song_list", "#main"); 34 | songs.each(function (index, song) { 35 | var songLinkElem = $(song).find(".song_link"); 36 | songLinkElem.each(function (i, s) { 37 | var songLink = $(s).attr("href"); 38 | var songName = StringUtils.removeWhiteSpacesAndNewLines($(s).children(".title_with_artists").text()); 39 | var rapSong = new Song(songName, artistLink, songLink); 40 | 41 | if (index === 0) { 42 | //This element represents the favourite songs of the artist 43 | rapArtist.addPopularSong(rapSong); 44 | } 45 | rapArtist.addSong(rapSong); 46 | }); 47 | 48 | }); 49 | 50 | return rapArtist; 51 | 52 | } catch (e) { 53 | console.log("An error occured while trying to parse the artist: [html=" + html + "], error: " + e); 54 | return new Error("Unable to parse artist details results from RapGenius"); 55 | } 56 | } 57 | 58 | module.exports.parseArtistHTML = parseArtistHTML; -------------------------------------------------------------------------------- /src/parsers/LyricsParser.js: -------------------------------------------------------------------------------- 1 | var cheerio = require("cheerio"), 2 | Lyrics = require("../model/Lyrics"), 3 | StringUtils = require("../util/StringUtils"); 4 | 5 | function parseLyricsHTML(html, type) { 6 | try { 7 | var $ = cheerio.load(html); 8 | 9 | //Let's extract main and featured artists 10 | var mainArtist = $(".song_header .title_and_authors .text_artist > a", "#main").text().replace(/^\s+|\s+$/g, ''); 11 | var songTitle = $(".song_header .title_and_authors .text_title", "#main").text(); 12 | 13 | //trimming song title string 14 | songTitle = songTitle.replace("\n", ""); 15 | songTitle = songTitle.replace(/^\s+|\s+$/g, ''); 16 | 17 | var ftList = []; 18 | var featured = $(".song_header .featured_artists > a", "#main"); 19 | featured.each(function (index, featuringArtist) { 20 | var ftArtistName = $(featuringArtist).text(); 21 | ftList.push(ftArtistName); 22 | }); 23 | 24 | var prodList = []; 25 | var producers = $(".song_header .producer_artists > a", "#main"); 26 | producers.each(function (index, producingArtist) { 27 | var prodArtist = $(producingArtist).text(); 28 | prodList.push(prodArtist); 29 | }); 30 | 31 | var lyricsContainer = $(".lyrics_container", "#main"); 32 | if (lyricsContainer.length <= 0) { 33 | return new Error("Unable to parse lyrics: lyrics_container does not exist!"); 34 | } 35 | var rapLyrics = null; 36 | 37 | var currentSection = new Lyrics.Section("[Empty Section]"); 38 | 39 | //We definitely have some lyrics down there... 40 | lyricsContainer.each(function (index, container) { 41 | //The lyrics class holds the paragraphs that contain the lyrics 42 | var lyricsElems = $(container).find(".lyrics"); 43 | var songId = parseInt($(lyricsElems.first()).attr("data-id")); 44 | rapLyrics = new Lyrics.Lyrics(songId, 10); 45 | rapLyrics.songTitle = songTitle; 46 | rapLyrics.mainArtist = mainArtist; 47 | rapLyrics.featuringArtists = ftList; 48 | rapLyrics.producingArtists = prodList; 49 | 50 | var currentVerses = null; 51 | 52 | //Parsing function - what really does the job 53 | var parserFunc = function (index, paragraphElem) { 54 | var paragraphParser = function (paragraphContent) { 55 | if (paragraphContent.type === "text") { 56 | var parsed = StringUtils.removeWhiteSpacesAndNewLines(paragraphContent.data); 57 | 58 | //check if parsed content is a section 59 | if (/^\[.*\]$/.test(parsed)) { 60 | currentSection = new Lyrics.Section(parsed); 61 | rapLyrics.addSection(currentSection); 62 | } else { 63 | //Not a section name, therefore this must be text 64 | //However we only want to add non empty strings 65 | var parsedNotEmpty = parsed.length > 0; 66 | 67 | if (!currentVerses && parsedNotEmpty) { 68 | //Add verses to section 69 | currentVerses = new Lyrics.Verses(-1); 70 | currentSection.addVerses(currentVerses); 71 | } 72 | //Now add content to verses object 73 | if (parsedNotEmpty) { 74 | currentVerses.addContent(parsed); 75 | } 76 | } 77 | } else if (paragraphContent.type === "tag" && paragraphContent.name === "br") { 78 | if (currentVerses) { 79 | currentVerses.addContent("\n"); 80 | } 81 | } else if (paragraphContent.type === "tag" && paragraphContent.name === "a") { 82 | //We have stumbled upon an annotate lyrics block 83 | var lyricsId = parseInt($(paragraphContent).attr("data-id"), 10); 84 | currentVerses = new Lyrics.Verses(lyricsId); 85 | currentSection.addVerses(currentVerses); 86 | 87 | //We now recursively process the text that is inside the tag as it contains the lyrics 88 | paragraphContent.children.forEach(paragraphParser); 89 | } 90 | 91 | }; 92 | 93 | paragraphElem.children.forEach(paragraphParser); 94 | 95 | }; 96 | 97 | //All lyrics are now contained in paragraphs 98 | lyricsElems.find("p").each(parserFunc); 99 | 100 | }); 101 | if (rapLyrics.sections.length === 0){ 102 | rapLyrics.addSection(currentSection); 103 | } 104 | return rapLyrics; 105 | } catch (e) { 106 | console.log("An error occurred while trying to parse the lyrics: [html=" + html + "], :\n" + e); 107 | return new Error("Unable to parse lyrics from RapGenius"); 108 | } 109 | } 110 | 111 | 112 | function parseLyricsExplanationJSON(lyricsJson) { 113 | try { 114 | var explanations = {}; 115 | var keys = Object.keys(lyricsJson); 116 | keys.forEach(function (id) { 117 | var html = lyricsJson[id]; 118 | var $ = cheerio.load(html); 119 | var bodyText = $(".body_text", ".annotation_container").text(); 120 | explanations[id] = StringUtils.removeWhiteSpacesAndNewLines(bodyText); 121 | }); 122 | 123 | return explanations; 124 | } catch (e) { 125 | console.log("An error occurred while trying to parse the lyrics explanations [lyrics-explanations=" + lyricsJson + 126 | "]: \n" + e); 127 | return new Error("Unable to extract lyrics explanations"); 128 | } 129 | } 130 | 131 | module.exports.parseLyricsHTML = parseLyricsHTML; 132 | module.exports.parseLyricsExplanationJSON = parseLyricsExplanationJSON; 133 | -------------------------------------------------------------------------------- /src/parsers/SongsParser.js: -------------------------------------------------------------------------------- 1 | var cheerio = require("cheerio"), 2 | CONSTANTS = require("../constants/Constants"), 3 | Song = require("../model/Song"), 4 | StringUtils = require("../util/StringUtils"); 5 | 6 | 7 | function parseSongHTML(html, type) { 8 | //TODO Check we are dealing with proper HTML 9 | //or something that looks like HTML, otherwise throw an error 10 | try { 11 | var urls = CONSTANTS.Type2URLs[type]; 12 | var $ = cheerio.load(html); 13 | var songs = $(".song_link", "#main"); 14 | 15 | var rapSongArray = new Array(songs.length); 16 | 17 | songs.each(function (index, song) { 18 | var link = $(this).attr("href"); 19 | // Adding the http prefix if the link does not contain it 20 | if (!/^http/.test(link)){ 21 | link = urls.base_url + link; 22 | } 23 | var elem = $(this).find(".title_with_artists"); 24 | 25 | var artists = elem.find(".artist_name").text(); 26 | var songName = elem.find(".song_title").text(); 27 | 28 | rapSongArray[index] = new Song(songName, artists, link); 29 | }); 30 | 31 | return rapSongArray; 32 | } catch (e) { 33 | console.log("An error occured while trying to parse the songs: [html=" + html + "]"); 34 | return new Error("Unable to parse song search results from RapGenius"); 35 | } 36 | } 37 | 38 | module.exports.parseSongHTML = parseSongHTML; 39 | -------------------------------------------------------------------------------- /src/util/StringUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple String utility file 3 | * Keeping it lean and mean 4 | */ 5 | 6 | function trim(str) { 7 | if (checkVarIsAString(str)) { 8 | //Remove leading and trailing whitespaces 9 | str = str.replace(/^\s*/, ""); 10 | str = str.replace(/\s*$/, ""); 11 | 12 | return str; 13 | } 14 | 15 | return ""; 16 | } 17 | 18 | function removeWhiteSpacesAndNewLines(str) { 19 | if (checkVarIsAString(str)) { 20 | str = str.replace(/^\s*\n*\s*/, ""); 21 | str = str.replace(/\s*\n*\s*$/, ""); 22 | 23 | return str; 24 | } 25 | 26 | return ""; 27 | } 28 | 29 | function checkVarIsAString(v) { 30 | return v && typeof v === "string"; 31 | } 32 | 33 | 34 | module.exports.trim = trim; 35 | module.exports.removeWhiteSpacesAndNewLines = removeWhiteSpacesAndNewLines; -------------------------------------------------------------------------------- /test/rapGeniusClientTest.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | geniusClient = require("./../src/geniusClient"), 3 | assert = require("assert"); 4 | util = require("util"), 5 | Song = require("./../src/model/Song"), 6 | Artist = require("./../src/model/Artist"), 7 | Lyrics = require("./../src/model/Lyrics"); 8 | 9 | 10 | vows.describe("Search checks").addBatch({ 11 | "when searching for a given song": { 12 | topic: function () { 13 | geniusClient.searchSong('Liquid Swords', "rap", this.callback); 14 | }, 15 | 16 | "A valid response body is returned": function (err, response) { 17 | //Make sure we receive something 18 | 19 | assert.ok(!err); 20 | assert.ok(!(response instanceof Error)); 21 | assert.ok(response instanceof Array); 22 | assert.ok(response[0] instanceof Song); 23 | assert.ok(response[0].name); 24 | assert.ok(response[0].artists); 25 | assert.ok(response[0].link); 26 | } 27 | 28 | }, 29 | "when searching one of the artist's name": { 30 | topic: function () { 31 | geniusClient.searchArtist("The Genius", "rap", this.callback); 32 | }, 33 | 34 | "A valid response body is returned": function (err, response) { 35 | assert.ok(!err); 36 | assert.ok(!(response instanceof Error)); 37 | assert.ok(response instanceof Artist); 38 | assert.ok(response.popularSongs.length > 0); 39 | assert.ok(response.songs.length > 0); 40 | assert.deepEqual(response.name, "Genius"); 41 | assert.deepEqual(response.link.toUpperCase(), "http://genius.com/artists/Genius".toUpperCase()); 42 | } 43 | }, 44 | "when searching one artist's real name": { 45 | topic: function () { 46 | geniusClient.searchArtist("GZA", "rap", this.callback); 47 | }, 48 | 49 | "A valid response body is returned": function (err, response) { 50 | assert.ok(!err); 51 | assert.ok(!(response instanceof Error)); 52 | assert.ok(response instanceof Artist); 53 | assert.ok(response.popularSongs.length > 0); 54 | assert.ok(response.songs.length > 0); 55 | assert.deepEqual(response.name, "GZA"); 56 | assert.deepEqual(response.link.toUpperCase(), "http://genius.com/artists/Gza".toUpperCase()); 57 | } 58 | }, 59 | "when searching an artist that does not exist": { 60 | topic: function () { 61 | geniusClient.searchArtist("opu8psdjchlk", "rap", this.callback); 62 | }, 63 | 64 | "An error object is returned": function (err, response) { 65 | assert.ok(err); 66 | } 67 | }, 68 | "When searching for the lyrics of given song":{ 69 | topic: function(){ 70 | geniusClient.searchSongLyrics("/Kanye-west-gorgeous-lyrics", "rap", this.callback); 71 | }, 72 | 73 | "The parsed lyrics are returned in an object": function(err, response){ assert.ok(!err); 74 | assert.ok(!(response instanceof Error)); 75 | assert.ok(response instanceof Lyrics.Lyrics); 76 | assert.deepEqual(response.songId, 1791); 77 | assert.deepEqual(response.songTitle, "Gorgeous"); 78 | assert.deepEqual(response.mainArtist, "Kanye West"); 79 | assert.deepEqual(response.producingArtists, ["Kanye West", "Mike Dean", "No I.D."]); 80 | assert.deepEqual(response.featuringArtists, ["Kid Cudi", "Raekwon"]); 81 | assert.ok(response.sections.length > 0); 82 | assert.deepEqual(response.sections[0].name, "[Produced by Kanye West, Mike Dean & No I.D.]"); 83 | } 84 | }, 85 | 86 | "when searching for the meaning of a song's lyrics":{ 87 | topic: function(){ 88 | geniusClient.searchLyricsExplanation(3681, "rap", this.callback); 89 | }, 90 | 91 | "An explanation object is returned": function(err, response){ 92 | assert.ok(!err); 93 | assert.ok(response[107258]) ; 94 | assert.deepEqual(response[310425], "Really though, why would anyone try to fuck with them?"); 95 | } 96 | }, 97 | 98 | "when searching for a song's lyrics and their meaning":{ 99 | topic: function(){ 100 | geniusClient.searchLyricsAndExplanations("/Raekwon-knowledge-god-lyrics", "rap", this.callback); 101 | }, 102 | 103 | "A returned tuple of lyrics and explanations is returned": function(err, response){ 104 | assert.ok(!err); 105 | assert.ok(response.lyrics instanceof Lyrics.Lyrics); 106 | assert.deepEqual(response.explanations[310425], "Really though, why would anyone try to fuck with them?"); 107 | } 108 | }, 109 | "When searching for the lyrics of a song that does not exist":{ 110 | topic: function(){ 111 | geniusClient.searchSongLyrics("/DOES-NOT-EXIST-LYRICS", "rap", this.callback); 112 | }, 113 | 114 | "An error is returned": function(err, response){ 115 | assert.ok(err); 116 | assert.ok(!(err instanceof Lyrics.Lyrics)); 117 | assert.ok(err instanceof Error); 118 | } 119 | } 120 | }).run(); 121 | 122 | -------------------------------------------------------------------------------- /test/rockGeniusClientTest.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | geniusClient = require("./../src/geniusClient"), 3 | assert = require("assert"); 4 | util = require("util"), 5 | Song = require("./../src/model/Song"), 6 | Artist = require("./../src/model/Artist"), 7 | Lyrics = require("./../src/model/Lyrics"); 8 | 9 | vows.describe("Search checks").addBatch({ 10 | "when searching for a given rock song":{ 11 | topic: function(){ 12 | geniusClient.searchSong("Born To Run", "rock", this.callback); 13 | }, 14 | 15 | "A valid response body is returned": function(err, response){ 16 | assert.ok(!err); 17 | assert.ok(!(response instanceof Error)); 18 | assert.ok(response instanceof Array); 19 | assert.ok(response[0] instanceof Song); 20 | assert.ok(response[0].name); 21 | assert.ok(response[0].artists); 22 | assert.ok(response[0].link); 23 | } 24 | }, 25 | "when searching a valid rock artist's name": { 26 | topic: function () { 27 | geniusClient.searchArtist("Bruce Springsteen", "rock", this.callback); 28 | }, 29 | 30 | "A valid response body is returned": function (err, response) { 31 | assert.ok(!err); 32 | assert.ok(!(response instanceof Error)); 33 | assert.ok(response instanceof Artist); 34 | assert.ok(response.popularSongs.length > 0); 35 | assert.ok(response.songs.length > 0); 36 | assert.deepEqual(response.name, "Bruce Springsteen"); 37 | assert.deepEqual(response.link.toUpperCase(), "http://rock.genius.com/artists/Bruce-Springsteen".toUpperCase()); 38 | } 39 | }, 40 | 41 | "When searching for the lyrics of given song":{ 42 | topic: function(){ 43 | geniusClient.searchSongLyrics("/Bruce-springsteen-born-to-run-lyrics", "rock", this.callback); 44 | }, 45 | 46 | "The parsed lyrics are returned in an object": function(err, response){ 47 | assert.ok(!err); 48 | assert.ok(!(response instanceof Error)); 49 | assert.ok(response instanceof Lyrics.Lyrics); 50 | assert.deepEqual(response.songId, 75426); 51 | assert.deepEqual(response.songTitle, "Born to Run"); 52 | assert.deepEqual(response.mainArtist, "Bruce Springsteen"); 53 | assert.deepEqual(response.producingArtists, []); 54 | assert.deepEqual(response.featuringArtists, []); 55 | assert.ok(response.sections.length > 0); 56 | assert.deepEqual(response.sections[0].name, "[Empty Section]"); 57 | assert.ok(/^In the day we sweat it out in the streets of a.*/.test(response.sections[0].verses[0].content)); 58 | } 59 | } 60 | 61 | 62 | }).run(); 63 | -------------------------------------------------------------------------------- /test/unitTests.js: -------------------------------------------------------------------------------- 1 | var StringUtils = require("./../src/util/StringUtils"), 2 | Lyrics = require("./../src/model/Lyrics"), 3 | vows = require("vows"), 4 | assert = require("assert"); 5 | 6 | vows.describe("Unit tests").addBatch({ 7 | "When trimming a non string": { 8 | topic: function () { 9 | return StringUtils.trim({a: 2}); 10 | }, 11 | 12 | "Empty String is returned": function (topic) { 13 | assert.deepEqual(topic, ""); 14 | } 15 | }, 16 | 17 | "When trimming null": { 18 | topic: function () { 19 | return StringUtils.trim(null); 20 | }, 21 | 22 | "Empty String is returned": function (topic) { 23 | assert.deepEqual(topic, ""); 24 | } 25 | }, 26 | 27 | "When trimming string ' ABC '": { 28 | topic: function () { 29 | return StringUtils.trim(" ABC "); 30 | }, 31 | 32 | "The string 'ABC' is returned": function (topic) { 33 | assert.deepEqual(topic, "ABC"); 34 | } 35 | }, 36 | 37 | "When trimming string 'ABC '": { 38 | topic: function () { 39 | return StringUtils.trim("ABC "); 40 | }, 41 | 42 | "The string 'ABC' is returned": function (topic) { 43 | assert.deepEqual(topic, "ABC"); 44 | } 45 | }, 46 | 47 | "When trimming string ' ABC'": { 48 | topic: function () { 49 | return StringUtils.trim(" ABC"); 50 | }, 51 | 52 | "The string 'ABC' is returned": function (topic) { 53 | assert.deepEqual(topic, "ABC"); 54 | } 55 | } 56 | 57 | }).addBatch({ 58 | "When removing spaces and new lines from non string": { 59 | topic: function () { 60 | return StringUtils.removeWhiteSpacesAndNewLines({}); 61 | }, 62 | 63 | "An empty string is returned": function (topic) { 64 | assert.deepEqual(topic, ""); 65 | } 66 | }, 67 | 68 | "When removing spaces and new lines from null": { 69 | topic: function () { 70 | return StringUtils.removeWhiteSpacesAndNewLines(null); 71 | }, 72 | 73 | "An empty string is returned": function (topic) { 74 | assert.deepEqual(topic, ""); 75 | } 76 | }, 77 | 78 | "When removing spaces and new lines from string '\\n \\n ABC \\n '": { 79 | topic: function () { 80 | return StringUtils.removeWhiteSpacesAndNewLines("\n \n ABC \n "); 81 | }, 82 | 83 | "The string 'ABC' is returned": function (topic) { 84 | assert.deepEqual(topic, "ABC"); 85 | } 86 | } 87 | }).addBatch({ 88 | "When requesting full lyrics without section name":{ 89 | topic: function(){ 90 | var lyrics1 = "Man these tests put me on mad edge\n" + 91 | "Wanna skip'em but gotta keep my pledge\n"; 92 | var lyrics2 = "When I put this hat on to work some node\n" + 93 | "I type keyboard bangin' to create some code\n"; 94 | 95 | var verse1 = new Lyrics.Verses(0); 96 | var verse2 = new Lyrics.Verses(0); 97 | verse1.addContent(lyrics1); 98 | verse2.addContent(lyrics2); 99 | 100 | var section1 = new Lyrics.Section("[Intro-1]\n"); 101 | var section2 = new Lyrics.Section("[Intro-2]\n"); 102 | section1.addVerses(verse1); 103 | section2.addVerses(verse2); 104 | 105 | var rapLyrics = new Lyrics.Lyrics(0); 106 | rapLyrics.addSection(section1); 107 | rapLyrics.addSection(section2); 108 | 109 | return rapLyrics; 110 | }, 111 | 112 | "The full lyrics with no sections are returned" : function(topic){ 113 | assert.ok(topic instanceof Lyrics.Lyrics); 114 | var lyrics = "Man these tests put me on mad edge\n" + 115 | "Wanna skip'em but gotta keep my pledge\n" + 116 | "When I put this hat on to work some node\n" + 117 | "I type keyboard bangin' to create some code\n"; 118 | assert.deepEqual(topic.getFullLyrics(false), lyrics); 119 | } 120 | }, 121 | 122 | "When requesting full lyrics with section names":{ 123 | topic: function(){ 124 | var lyrics1 = "Man these tests put me on mad edge\n" + 125 | "Wanna skip'em but gotta keep my pledge\n"; 126 | var lyrics2 = "When I put this hat on to work some node\n" + 127 | "I type keyboard bangin' to create some code\n"; 128 | 129 | var verse1 = new Lyrics.Verses(0); 130 | var verse2 = new Lyrics.Verses(0); 131 | verse1.addContent(lyrics1); 132 | verse2.addContent(lyrics2); 133 | 134 | var section1 = new Lyrics.Section("[Intro-1]"); 135 | var section2 = new Lyrics.Section("[Intro-2]"); 136 | section1.addVerses(verse1); 137 | section2.addVerses(verse2); 138 | 139 | var rapLyrics = new Lyrics.Lyrics(0); 140 | rapLyrics.addSection(section1); 141 | rapLyrics.addSection(section2); 142 | 143 | return rapLyrics; 144 | }, 145 | 146 | "The full lyrics with no sections are returned" : function(topic){ 147 | assert.ok(topic instanceof Lyrics.Lyrics); 148 | var lyrics = "[Intro-1]\n" + 149 | "Man these tests put me on mad edge\n" + 150 | "Wanna skip'em but gotta keep my pledge\n" + 151 | "[Intro-2]\n" + 152 | "When I put this hat on to work some node\n" + 153 | "I type keyboard bangin' to create some code\n"; 154 | assert.deepEqual(topic.getFullLyrics(true), lyrics); 155 | } 156 | }, 157 | 158 | "When adding explanation to lyrics": { 159 | topic: function(){ 160 | var lyrics1 = "Man these tests put me on mad edge\n" + 161 | "Wanna skip'em but gotta keep my pledge\n"; 162 | var lyrics2 = "When I put this hat on to work some node\n" + 163 | "I type keyboard bangin' to create some code\n"; 164 | 165 | var verse1 = new Lyrics.Verses(1); 166 | var verse2 = new Lyrics.Verses(2); 167 | verse1.addContent(lyrics1); 168 | verse2.addContent(lyrics2); 169 | 170 | var section1 = new Lyrics.Section("[Intro-1]\n"); 171 | var section2 = new Lyrics.Section("[Intro-2]\n"); 172 | section1.addVerses(verse1); 173 | section2.addVerses(verse2); 174 | 175 | var rapLyrics = new Lyrics.Lyrics(0); 176 | rapLyrics.addSection(section1); 177 | rapLyrics.addSection(section2); 178 | 179 | var explanations = {"1": "I am talking about these unit tests driving me crazy", 180 | "2": "But when I wear my productivity hat, I ship some code quick"}; 181 | rapLyrics.addExplanations(explanations); 182 | 183 | return rapLyrics; 184 | }, 185 | 186 | "The verses contain the explanation": function(lyrics){ 187 | assert.ok(lyrics instanceof Lyrics.Lyrics); 188 | assert.deepEqual(lyrics.sections[0].verses[0].explanation, "I am talking about these unit tests driving me crazy"); 189 | assert.deepEqual(lyrics.sections[1].verses[0].explanation, "But when I wear my productivity hat, I ship some code quick"); 190 | } 191 | } 192 | 193 | }).run(); --------------------------------------------------------------------------------