├── .gitignore ├── screen.png ├── images └── logo.png ├── js ├── spotify.js ├── auth.js ├── config.js └── script.js ├── README.md ├── LICENSE ├── index.html └── css └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaaes/albums-availability/HEAD/screen.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaaes/albums-availability/HEAD/images/logo.png -------------------------------------------------------------------------------- /js/spotify.js: -------------------------------------------------------------------------------- 1 | var SpotifyApi = { 2 | 3 | sendRequest: function(url, callback) { 4 | 5 | var accessToken = Config.getValidToken() 6 | if (accessToken == null) { 7 | var error = 'unauthorised'; 8 | callback(error); 9 | return; 10 | } 11 | 12 | d3.json(url) 13 | .header('Authorization', 'Bearer ' + accessToken) 14 | .get(callback); 15 | } 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Albums Availability Map 2 | ----------------------- 3 | 4 | A visualisation for albums and their availability in Spotify. 5 | 6 | This project uses [Spotify Web API](https://developer.spotify.com/web-api/) to get the data 7 | and [Google Charts](https://developers.google.com/chart/) to display them. 8 | There's also a tiny bit of the amazing [D3.js](http://d3js.org/) to help process the data. 9 | 10 | To check an album paste the uri to the search box or drag an album from Spotify client and drop it 11 | somewhere on the page. 12 | 13 | ![Screenshot of the map](/screen.png) 14 | -------------------------------------------------------------------------------- /js/auth.js: -------------------------------------------------------------------------------- 1 | var Auth = { 2 | getAuthUrl: function(siteUrl) { 3 | 4 | var clientId = '54e0e5bde5be499a94ecf7b31c1da2f1'; 5 | var redirectUri = encodeURIComponent(siteUrl + '?auth_callback'); 6 | 7 | return url = 'https://accounts.spotify.com/authorize?client_id=' + clientId + '&redirect_uri=' + redirectUri + '&response_type=token'; 8 | }, 9 | 10 | parseResponse: function(url) { 11 | var hash = url.hash; 12 | var response = this._parseHash(hash); 13 | if (response != null) { 14 | Config.setToken(response['access_token']); 15 | Config.setExpiresAt(response['expires_in']); 16 | } 17 | }, 18 | 19 | _parseHash: function(hash) { 20 | if (!hash) { 21 | return null; 22 | } 23 | 24 | parsed = hash.replace('#', '') 25 | .split('&') 26 | .map(function(el) { 27 | return el.split('='); 28 | }) 29 | .reduce(function(acc, el) { 30 | acc[el[0]] = el[1]; 31 | return acc; 32 | }, {}); 33 | 34 | return parsed; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /js/config.js: -------------------------------------------------------------------------------- 1 | var Config = { 2 | setLastSearch: function(id) { 3 | localStorage.setItem('lastSearch', id); 4 | window.location.hash = id; 5 | }, 6 | 7 | getLastSearch: function() { 8 | return window.location.hash.substring(1) || localStorage.getItem('lastSearch'); 9 | }, 10 | 11 | setExactSearch: function(useExact) { 12 | localStorage.setItem('exactSearch', mode); 13 | }, 14 | 15 | getExactSearch: function() { 16 | return localStorage.getItem('exactSearch') === 'true'; 17 | }, 18 | 19 | getValidToken: function() { 20 | var expiresAt = parseInt(localStorage.getItem('token_expires_at'), 10); 21 | var token = localStorage.getItem('token'); 22 | var now = Date.now(); 23 | return !!token && !!expiresAt && expiresAt > now ? token : null; 24 | }, 25 | 26 | setToken: function(accessToken) { 27 | localStorage.setItem('token', accessToken); 28 | }, 29 | 30 | setExpiresAt: function(expiresIn) { 31 | var now = Date.now(); 32 | var expiresAt = now + parseInt(expiresIn, 10) * 1000; 33 | localStorage.setItem('token_expires_at', expiresAt); 34 | } 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kasia Drzyzga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Album Availability Map 5 | 6 | 7 | 8 | 9 | 38 |
39 |
40 |
41 |
42 |

43 |

44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 |

Only albums are supported

54 |
55 | 56 |
57 |

You need a Spotify token to be able to get albums data.

58 | 59 |
60 | 61 | 65 | 66 | 71 | 72 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'proxima_nova'; 3 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_light_0.eot'); 4 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_light_0.eot?#iefix') format('embedded-opentype'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_light_0.woff') format('woff'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_light_0.ttf') format('truetype'); 5 | font-weight: 200; 6 | font-style: normal; 7 | } 8 | @font-face { 9 | font-family: 'proxima_nova'; 10 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_thin_0.eot'); 11 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_thin_0.eot?#iefix') format('embedded-opentype'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_thin_0.woff') format('woff'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_thin_0.ttf') format('truetype'); 12 | font-weight: 100; 13 | font-style: normal; 14 | } 15 | @font-face { 16 | font-family: 'proxima_nova'; 17 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_regular_0.eot'); 18 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_regular_0.eot?#iefix') format('embedded-opentype'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_regular_0.woff') format('woff'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_regular_0.ttf') format('truetype'); 19 | font-weight: 400; 20 | font-style: normal; 21 | } 22 | @font-face { 23 | font-family: 'proxima_nova'; 24 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_semibold_0.eot'); 25 | src: url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_semibold_0.eot?#iefix') format('embedded-opentype'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_semibold_0.woff') format('woff'), url('https://d3nkxvtkt5c8vi.cloudfront.net/3.0.1/fonts/proxima_nova_semibold_0.ttf') format('truetype'); 26 | font-weight: 600; 27 | font-style: normal; 28 | } 29 | 30 | body { 31 | background: #121314; 32 | font-weight: 100; 33 | letter-spacing: 1px; 34 | color: #dfe0e6; 35 | } 36 | 37 | h1, h2, h3, h4, h5, h6 { 38 | font-weight: 100; 39 | letter-spacing: 1px; 40 | } 41 | 42 | a { 43 | color: #84bd00; 44 | text-decoration: none; 45 | } 46 | a:hover, 47 | a:focus { 48 | color: #648f00; 49 | text-decoration: underline; 50 | } 51 | a:focus { 52 | outline: thin dotted; 53 | outline: 5px auto -webkit-focus-ring-color; 54 | outline-offset: -2px; 55 | } 56 | 57 | .navbar-logo { 58 | width: 134px; 59 | height: 40px; 60 | display: inline-block; 61 | text-indent: 9999px; 62 | position: relative; 63 | top: -10px; 64 | background-image: url('../images/logo.png'); 65 | } 66 | 67 | .navbar-default .navbar-brand:hover, 68 | .navbar-default .navbar-brand:focus { 69 | color: inherit; 70 | } 71 | 72 | .navbar-default { 73 | background-color: rgba(0,0,0,.5); 74 | border-color: #000; 75 | } 76 | 77 | .navbar-default .navbar-brand { 78 | color: inherit; 79 | font-size: 32px; 80 | } 81 | 82 | .navbar .container { 83 | width: 100%; 84 | } 85 | 86 | .navbar-form .form-control { 87 | width: 290px; 88 | } 89 | 90 | .container.regions { 91 | width: 100%; 92 | position: relative; 93 | } 94 | 95 | .regions .info { 96 | background-color: rgba(50,50,50,.3); 97 | box-shadow: 0 0 50px rgba(255,255,255,.1); 98 | } 99 | 100 | .glyphicon-search { 101 | top: 0; 102 | } 103 | 104 | .entity-info { 105 | display: inline-block; 106 | position: absolute; 107 | padding: 1em; 108 | margin: 15px; 109 | z-index: 2; 110 | left: 0; 111 | bottom: 0; 112 | background: rgba(0,0,0,.3) 113 | } 114 | 115 | .entity-info h1, 116 | .entity-info h2 { 117 | margin: 0; 118 | } 119 | 120 | #regions-map { 121 | height: 100%; 122 | width: 80%; 123 | z-index: 1; 124 | position: fixed; 125 | display: none; 126 | } 127 | 128 | #regions-info { 129 | padding-top: 50px; 130 | position: relative; 131 | z-index: 2; 132 | border-right: solid 1px rgba(0,0,0,.1); 133 | } 134 | 135 | .albums-info > div { 136 | padding: 0 10px 5px 10px; 137 | margin-bottom: 5px; 138 | } 139 | 140 | .albums-info > h3 { 141 | font-weight: 200; 142 | padding: 0 10px; 143 | margin-top: 10px; 144 | margin-bottom: 10px; 145 | } 146 | 147 | .albums-info .alert-warning { 148 | font-size: .9em; 149 | padding-top: .5em; 150 | padding-bottom: .5em; 151 | } 152 | 153 | .album-info.searched { 154 | border-left: solid 5px #648f00; 155 | } 156 | 157 | .album-info:hover { 158 | background: rgba(255,255,255,.1); 159 | } 160 | 161 | .albums-info p { 162 | margin: .4em 0; 163 | } 164 | 165 | .albums-info .explicit { 166 | font-size: .7em; 167 | text-transform: uppercase; 168 | border: solid 1px #999; 169 | line-height: .6em; 170 | padding: .1em .1em 0 .3em; 171 | letter-spacing: .2em; 172 | } 173 | 174 | .albums-info .uri { 175 | letter-spacing: .05em; 176 | color: #999; 177 | } 178 | 179 | .albums-info .uri a { 180 | color: inherit; 181 | } 182 | 183 | .albums-info .cover { 184 | box-shadow: 0 0 1px rgba(255,255,255,.3); 185 | width: 64px; 186 | height: 64px; 187 | margin-top: 5px; 188 | display: inline-block; 189 | float: left; 190 | border-radius: 2px 191 | } 192 | 193 | .albums-info ul { 194 | height: 64px; 195 | margin-top: 5px; 196 | display: inline-block; 197 | padding-left: 10px; 198 | list-style: none; 199 | } 200 | 201 | .checkbox { 202 | white-space: nowrap; 203 | } 204 | 205 | .checkbox label { 206 | font-weight: 100; 207 | } 208 | 209 | .albums-info .copyrights { 210 | font-size: .85em; 211 | white-space: nowrap; 212 | overflow: hidden; 213 | text-overflow: ellipsis; 214 | margin: 0; 215 | } 216 | 217 | .albums-info .album-info:hover .copyrights { 218 | white-space: normal; 219 | } 220 | 221 | .albums-only, 222 | .get-token { 223 | display: none; 224 | width: 100%; 225 | position: absolute; 226 | z-index: 2; 227 | top: 50%; 228 | text-align: center; 229 | } 230 | 231 | .invalid-search .albums-only, 232 | .no-token .get-token { 233 | display: block; 234 | } 235 | 236 | @media (min-width: 992px) { 237 | #regions-map { 238 | display: block; 239 | } 240 | 241 | .entity-info { 242 | position: fixed; 243 | } 244 | } -------------------------------------------------------------------------------- /js/script.js: -------------------------------------------------------------------------------- 1 | google.load("visualization", "1", {packages:["geochart"]}); 2 | google.setOnLoadCallback(onGoogleLoaded); 3 | 4 | function onGoogleLoaded() { 5 | 6 | var SITE_URL = 'https://kaaes.github.io/albums-availability'; 7 | 8 | var ALL_MARKETS = { 9 | "AD": "Andorra", 10 | "AR": "Argentina", 11 | "AU": "Australia", 12 | "AT": "Austria", 13 | "BE": "Belgium", 14 | "BO": "Bolivia", 15 | "BR": "Brazil", 16 | "BG": "Bulgaria", 17 | "CA": "Canada", 18 | "CL": "Chile", 19 | "CO": "Colombia", 20 | "CR": "Costa Rica", 21 | "CY": "Cyprus", 22 | "CZ": "Czech Republic", 23 | "DK": "Denmark", 24 | "DO": "Dominican Republic", 25 | "EC": "Ecuador", 26 | "SV": "El Salvador", 27 | "EE": "Estonia", 28 | "FI": "Finland", 29 | "FR": "France", 30 | "DE": "Germany", 31 | "GR": "Greece", 32 | "GT": "Guatemala", 33 | "HN": "Honduras", 34 | "HK": "Hong Kong", 35 | "HU": "Hungary", 36 | "ID": "Indonesia", 37 | "IS": "Iceland", 38 | "IE": "Republic of Ireland", 39 | "IT": "Italy", 40 | "JP": "Japan", 41 | "LV": "Latvia", 42 | "LI": "Liechtenstein", 43 | "LT": "Lithuania", 44 | "LU": "Luxembourg", 45 | "MY": "Malaysia", 46 | "MT": "Malta", 47 | "MX": "Mexico", 48 | "MC": "Monaco", 49 | "NL": "Netherlands", 50 | "NZ": "New Zealand", 51 | "NI": "Nicaragua", 52 | "NO": "Norway", 53 | "PA": "Panama", 54 | "PY": "Paraguay", 55 | "PE": "Peru", 56 | "PH": "Philippines", 57 | "PL": "Poland", 58 | "PT": "Portugal", 59 | "ES": "Spain", 60 | "SG": "Singapore", 61 | "SK": "Slovakia", 62 | "SE": "Sweden", 63 | "CH": "Switzerland", 64 | "TW": "Taiwan", 65 | "TR": "Turkey", 66 | "GB": "United Kingdom", 67 | "US": "United States", 68 | "UY": "Uruguay" 69 | }; 70 | var SPOTIFY_API = 'https://api.spotify.com/v1'; 71 | 72 | var HIGHLIGHT_TIMEOUT = 100; 73 | 74 | var chart; 75 | 76 | var chartContainer = document.querySelector('#regions-map'); 77 | var searchForm = document.querySelector('#search-form'); 78 | var albumInfoBig = document.querySelector('#data-info'); 79 | var infoContainer = document.querySelector('#regions-info'); 80 | var exactSearchButton = document.querySelector('#exact-search-button'); 81 | var loginButton = document.querySelector('#login-button'); 82 | 83 | var albumInfoBigTemplate = document.querySelector('#template-album-info-big').innerHTML; 84 | var albumNotAvailableTemplate = document.querySelector('#template-album-not-available').innerHTML; 85 | var albumNotFoundTemplate = document.querySelector('#template-album-not-found').innerHTML; 86 | 87 | document.body.addEventListener('drop', handleDrop, false); 88 | document.body.addEventListener('dragover', handleDragOver, false); 89 | 90 | var timeoutHandler; 91 | infoContainer.addEventListener('mouseover', handleMouseover); 92 | infoContainer.addEventListener('mouseout', handleMouseout); 93 | 94 | loginButton.addEventListener('click', authenticate); 95 | 96 | exactSearchButton.checked = !Config.getExactSearch(); 97 | exactSearchButton.addEventListener('change', handleExactChange); 98 | searchForm.addEventListener('submit', handleFormSubmit); 99 | 100 | function handleFormSubmit(evt) { 101 | evt.preventDefault(); 102 | var searchValue = evt.target[0].value; 103 | var uri = parseUri(searchValue); 104 | validateAndPerformSearch(uri); 105 | } 106 | 107 | function handleExactChange(evt) { 108 | Config.setExactSearch(!evt.target.checked); 109 | var lastSearch = Config.getLastSearch(); 110 | if (lastSearch) { 111 | performSearch(lastSearch); 112 | } 113 | } 114 | 115 | function handleMouseover(evt) { 116 | clearTimeout(timeoutHandler); 117 | var self = this; 118 | timeoutHandler = setTimeout(function() { 119 | var path = evt.path; 120 | var current = self.dataset.active; 121 | for(var i = 0; i < path.length; i++) { 122 | if (path[i].dataset && path[i].dataset.albumid) { 123 | if (path[i].dataset.albumid != current) { 124 | current = path[i].dataset.albumid; 125 | self.dataset.active = current; 126 | highlight(current); 127 | } 128 | return; 129 | } 130 | } 131 | highlight(null); 132 | self.active = null; 133 | }, HIGHLIGHT_TIMEOUT); 134 | } 135 | 136 | function handleMouseout(evt) { 137 | clearTimeout(timeoutHandler); 138 | var self = this; 139 | timeoutHandler = setTimeout(function() { 140 | self.dataset.active = null; 141 | highlight(null); 142 | }, HIGHLIGHT_TIMEOUT); 143 | } 144 | 145 | function handleDragOver(evt) { 146 | evt.stopPropagation(); 147 | evt.preventDefault(); 148 | evt.dataTransfer.dropEffect = 'copy'; 149 | } 150 | 151 | function handleDrop(evt) { 152 | evt.stopPropagation(); 153 | evt.preventDefault(); 154 | 155 | var url = evt.dataTransfer.getData('text/plain'); 156 | if (!url) return; 157 | if (url.indexOf('open.spotify.com') > -1) { 158 | var parsed = parseUri(url.replace(/https?:\/\/open.spotify.com\//, 'spotify/'), '/'); 159 | } else { 160 | var parsed = parseUri(url); 161 | } 162 | 163 | validateAndPerformSearch(parsed); 164 | 165 | return false; 166 | } 167 | 168 | function parseUri(uri, delimiter) { 169 | var segments = uri.split(delimiter || ':'); 170 | var id, type; 171 | if (segments[1] === 'user') { 172 | type = segments[3]; 173 | id = segments[2] + ':' + segments[4]; 174 | } else if (segments[1] === 'album') { 175 | type = segments[1]; 176 | id = segments[2]; 177 | } 178 | return {type: type, id: id}; 179 | } 180 | 181 | function validateAndPerformSearch(parsedUri) { 182 | 183 | if (parsedUri.type == 'album') { 184 | document.body.classList.remove('invalid-search'); 185 | performSearch(parsedUri.id); 186 | } else { 187 | document.body.classList.add('invalid-search'); 188 | clearChart(); 189 | } 190 | } 191 | 192 | function clearChart() { 193 | drawChart([]); 194 | albumInfoBig.innerHTML = ''; 195 | infoContainer.innerHTML = ''; 196 | } 197 | 198 | function performSearch(id) { 199 | Config.setLastSearch(id); 200 | infoContainer.innerHTML = ''; 201 | 202 | SpotifyApi.sendRequest(SPOTIFY_API + '/albums/' + id, function(error, album) { 203 | 204 | if (error && error == 'unauthorised') { 205 | document.body.classList.add('no-token'); 206 | clearChart(); 207 | return; 208 | } 209 | 210 | fillInfo(album); 211 | 212 | function albumsCallback(result, next) { 213 | if (next) { 214 | getAlbums(next, album.name, albumsCallback, result); 215 | } else { 216 | fillInfoData(result, album); 217 | fillChartData(result); 218 | } 219 | } 220 | 221 | var uri = SPOTIFY_API + '/artists/' + album.artists[0].id + '/albums?album_type=' + album.album_type; 222 | 223 | var markets = []; 224 | var ids = []; 225 | var count = {}; 226 | if (album.available_markets.length) { 227 | markets.push(album.available_markets); 228 | ids.push(album.id); 229 | album.available_markets.forEach(function(market) { 230 | count[market] = 1; 231 | }); 232 | } 233 | 234 | getAlbums(uri, album.name, albumsCallback, { markets: markets, ids: ids, count: count }); 235 | }); 236 | } 237 | 238 | function getAlbums(uri, albumName, callback, result) { 239 | SpotifyApi.sendRequest(uri, function(error, albums) { 240 | albums.items.forEach(function(el) { 241 | var elName = el.name.toLowerCase().replace(/[,.;:?!"]/g, ''); 242 | var alName = albumName.toLowerCase().replace(/[,.;:?!"]/g, ''); 243 | var condition = Config.getExactSearch() ? 244 | el.name == alName : elName.indexOf(alName) == 0 || alName.indexOf(elName) == 0 245 | if (condition && result.ids.indexOf(el.id) == -1) { 246 | result.ids.push(el.id); 247 | result.markets.push(el.available_markets || []); 248 | if (el.available_markets) { 249 | el.available_markets.forEach(function(market) { 250 | result.count[market] = result.count[market] || 0; 251 | result.count[market]++; 252 | }); 253 | } 254 | } 255 | }); 256 | 257 | callback(result, albums.next); 258 | }); 259 | } 260 | 261 | function fillInfo(info) { 262 | var data = { 263 | albumTitle: info.name, 264 | albumType: info.album_type, 265 | artistName: info.artists.map(function(el) { return el.name; }).join(', ') 266 | } 267 | var rendered = Mustache.render(albumInfoBigTemplate, data); 268 | albumInfoBig.innerHTML = rendered; 269 | } 270 | 271 | var currentDataset; 272 | 273 | function highlight(id) { 274 | if (currentDataset) { 275 | var markets = []; 276 | var dataset = currentDataset.map(function(el) { 277 | var row = el.slice(0); 278 | row.spid = el.spid; 279 | if (row.spid && row.spid == id) { 280 | markets.push(row[0]) 281 | row[1] = 0.5; 282 | } 283 | return row; 284 | }); 285 | drawChart(dataset.filter(function(el) { 286 | return markets.indexOf(el[0]) == -1 || el.spid == id; 287 | })); 288 | } 289 | } 290 | 291 | function getAlbumInfo(name, albums, id) { 292 | var fragment = document.createDocumentFragment(); 293 | var title = document.createElement('h3'); 294 | title.classList.add("row"); 295 | title.classList.add("album-title"); 296 | title.innerHTML = name; 297 | 298 | fragment.appendChild(title); 299 | 300 | albums.forEach(function(album) { 301 | var div = document.createElement('div'); 302 | div.dataset.albumid = album.id; 303 | div.classList.add("album-info"); 304 | div.classList.add("row"); 305 | 306 | if (album.id == id) { 307 | div.classList.add("searched"); 308 | } 309 | 310 | var explicit = false; 311 | for(var i = 0; i < album.tracks.items.length; i++) { 312 | if (album.tracks.items[i].explicit) { 313 | explicit = true; 314 | break; 315 | } 316 | } 317 | 318 | var copyrights = d3.map(album.copyrights, function(d) { return d.type }); 319 | 320 | var html = ''; 321 | html += '' 322 | html += ''; 323 | html += ''; 324 | html += ''; 334 | html += '

' + album.available_markets.map(function(el) { 335 | return '' + el + ''; 336 | }).join(' ') + '

'; 337 | html += '

' + album.uri+ '

'; 338 | if (copyrights.get('C')) { 339 | html += '

© ' + copyrights.get('C').text; + '

'; 340 | } 341 | if (copyrights.get('P')) { 342 | html += '

℗ ' + copyrights.get('P').text + '

'; 343 | } 344 | div.innerHTML = html; 345 | 346 | fragment.appendChild(div); 347 | }); 348 | return fragment; 349 | } 350 | 351 | function fillInfoData(result, album) { 352 | var ids = result.ids; 353 | if (ids.length) { 354 | SpotifyApi.sendRequest(SPOTIFY_API + '/albums?ids=' + ids, function(error, albums) { 355 | console.log("Full found albums", albums.albums); 356 | var nest = d3.nest() 357 | .key(function(d) { return d.name; }) 358 | .sortKeys(function(a, b) { 359 | return a == album.name ? 1 : -1; 360 | }) 361 | .sortValues(function(a, b){ 362 | return a.id == album.id ? -1 : 1; 363 | }) 364 | .entries(albums.albums); 365 | 366 | nest.forEach(function(el) { 367 | infoContainer.insertBefore(getAlbumInfo(el.key, el.values, album.id), infoContainer.firstChild); 368 | }); 369 | 370 | if(!infoContainer.querySelector(".searched")) { 371 | exactMatchNotFound(album); 372 | } 373 | }); 374 | } else { 375 | exactMatchNotFound(album); 376 | } 377 | } 378 | 379 | function exactMatchNotFound(album) { 380 | var html = Mustache.render(albumNotFoundTemplate, { 381 | albumApiLink: album.href, 382 | albumUri: album.uri 383 | }) 384 | infoContainer.innerHTML = html + infoContainer.innerHTML; 385 | } 386 | 387 | function fillChartData(result) { 388 | var marketsList = result.markets; 389 | var ids = result.ids; 390 | var data = []; 391 | var allMarkets = Object.keys(ALL_MARKETS); 392 | 393 | marketsList.forEach(function(markets, i) { 394 | markets.forEach(function(market) { 395 | var index = allMarkets.indexOf(market); 396 | if (index > -1) { 397 | allMarkets.splice(allMarkets.indexOf(market), 1); 398 | } 399 | 400 | var name = market; 401 | if (ALL_MARKETS[market]) { 402 | name = ALL_MARKETS[market]; 403 | } else { 404 | console.log('Not in available markets', market); 405 | } 406 | 407 | var el = [name, result.count[market], result.count[market] + ' versions available']; 408 | el.spid = ids[i]; 409 | data.push(el); 410 | }); 411 | }); 412 | 413 | if (allMarkets.length) { 414 | console.log("Not available in: " + allMarkets); 415 | var html = Mustache.render(albumNotAvailableTemplate, { 416 | markets: allMarkets.map(function(el) { 417 | return { code: el, name: ALL_MARKETS[el]} 418 | }) 419 | }); 420 | infoContainer.innerHTML += html; 421 | } 422 | 423 | allMarkets.forEach(function(market) { 424 | data.push([ALL_MARKETS[market], 0, 'Not available']); 425 | }); 426 | 427 | console.log("Chart data", data); 428 | currentDataset = data; 429 | drawChart(data); 430 | } 431 | 432 | function drawChart(markets) { 433 | var chartData = new google.visualization.DataTable(); 434 | chartData.addColumn('string', 'Country'); 435 | chartData.addColumn('number', 'Available'); 436 | chartData.addColumn({type: 'string', role: 'tooltip'}); 437 | chartData.addRows(markets); 438 | 439 | var max = d3.max(markets, function(el) { return el[1] }) || 1; 440 | 441 | if (max < 1) max = 1; 442 | 443 | var options = { 444 | backgroundColor: 'transparent', 445 | datalessRegionColor: '#2e2f33', 446 | keepAspectRatio: true, 447 | colorAxis: { 448 | minValue: 0, 449 | colors: ['#8f2600', '#bdaa00', '#4B6601', '#B4D612'], 450 | values: [0, 0.5, 1, max] 451 | }, 452 | legend: 'none', 453 | tooltip: { 454 | showColorCode: false 455 | }, 456 | enableRegionInteractivity: true, 457 | animation: { 458 | duration: 1000, 459 | easing: 'out', 460 | } 461 | }; 462 | 463 | chart.draw(chartData, options); 464 | } 465 | 466 | function authenticate() { 467 | var authUrl = Auth.getAuthUrl(SITE_URL); 468 | window.location = authUrl; 469 | } 470 | 471 | if (location.search.indexOf('auth_callback') > 0) { 472 | Auth.parseResponse(location); 473 | window.location = SITE_URL; 474 | return; 475 | } 476 | 477 | chart = new google.visualization.GeoChart(chartContainer); 478 | 479 | performSearch(Config.getLastSearch() || "18qY7zpuNqeXNGywRysjxx"); 480 | } 481 | --------------------------------------------------------------------------------