committed by
							
								 GitHub
								GitHub
							
						
					
				
				
				  
				  No known key found for this signature in database
				  
				  	
						GPG Key ID: 4AEE18F83AFDEB23
				  	
				  
				
			
		
		
		
	
				 19 changed files with 809 additions and 424 deletions
			
			
		- 
					6.gitignore
- 
					2musicvis/settings.py
- 
					8recreate-db.txt
- 
					2requirements.txt
- 
					0sample-track-obj.py
- 
					5spotifyvis/admin.py
- 
					85spotifyvis/migrations/0001_initial.py
- 
					78spotifyvis/models.py
- 
					8spotifyvis/static/spotifyvis/css/dark_bg.css
- 
					137spotifyvis/static/spotifyvis/scripts/genre_graph.js
- 
					0spotifyvis/static/spotifyvis/scripts/user_data.js
- 
					141spotifyvis/templates/spotifyvis/audio_features.html
- 
					44spotifyvis/templates/spotifyvis/genre_graph.html
- 
					13spotifyvis/templates/spotifyvis/index.html
- 
					17spotifyvis/templates/spotifyvis/logged_in.html
- 
					9spotifyvis/templates/spotifyvis/user_data.html
- 
					20spotifyvis/urls.py
- 
					465spotifyvis/utils.py
- 
					171spotifyvis/views.py
| @ -0,0 +1,8 @@ | |||||
|  | # https://stackoverflow.com/a/34576062/8811872 | ||||
|  | 
 | ||||
|  | sudo su postgres | ||||
|  | psql | ||||
|  | drop database spotifyvis; | ||||
|  | create database spotifyvis with owner django; | ||||
|  | \q | ||||
|  | exit | ||||
| @ -1,3 +1,8 @@ | |||||
| from django.contrib import admin | from django.contrib import admin | ||||
|  | from .models import Track, Artist, AudioFeatures, User | ||||
| 
 | 
 | ||||
| # Register your models here. | # Register your models here. | ||||
|  | admin.site.register(Track) | ||||
|  | admin.site.register(Artist) | ||||
|  | admin.site.register(AudioFeatures) | ||||
|  | admin.site.register(User) | ||||
| @ -1,85 +0,0 @@ | |||||
| # Generated by Django 2.0.5 on 2018-06-03 23:01 |  | ||||
| 
 |  | ||||
| from django.db import migrations, models |  | ||||
| import django.db.models.deletion |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| class Migration(migrations.Migration): |  | ||||
| 
 |  | ||||
|     initial = True |  | ||||
| 
 |  | ||||
|     dependencies = [ |  | ||||
|     ] |  | ||||
| 
 |  | ||||
|     operations = [ |  | ||||
|         migrations.CreateModel( |  | ||||
|             name='Artist', |  | ||||
|             fields=[ |  | ||||
|                 ('artist_id', models.CharField(max_length=30, primary_key=True, serialize=False)), |  | ||||
|                 ('name', models.CharField(max_length=50, unique=True)), |  | ||||
|                 ('genre', models.CharField(max_length=20)), |  | ||||
|             ], |  | ||||
|             options={ |  | ||||
|                 'verbose_name': 'Artist', |  | ||||
|                 'verbose_name_plural': 'Artists', |  | ||||
|             }, |  | ||||
|         ), |  | ||||
|         migrations.CreateModel( |  | ||||
|             name='Track', |  | ||||
|             fields=[ |  | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |  | ||||
|                 ('track_id', models.CharField(max_length=30)), |  | ||||
|                 ('year', models.PositiveSmallIntegerField()), |  | ||||
|                 ('popularity', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('runtime', models.PositiveSmallIntegerField()), |  | ||||
|                 ('name', models.CharField(max_length=75)), |  | ||||
|             ], |  | ||||
|             options={ |  | ||||
|                 'verbose_name': 'Track', |  | ||||
|                 'verbose_name_plural': 'Tracks', |  | ||||
|             }, |  | ||||
|         ), |  | ||||
|         migrations.CreateModel( |  | ||||
|             name='User', |  | ||||
|             fields=[ |  | ||||
|                 ('user_id', models.CharField(max_length=30, primary_key=True, serialize=False)), |  | ||||
|                 ('username', models.CharField(max_length=30)), |  | ||||
|             ], |  | ||||
|             options={ |  | ||||
|                 'verbose_name': 'User', |  | ||||
|                 'verbose_name_plural': 'Users', |  | ||||
|             }, |  | ||||
|         ), |  | ||||
|         migrations.CreateModel( |  | ||||
|             name='AudioFeatures', |  | ||||
|             fields=[ |  | ||||
|                 ('track', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='spotifyvis.Track')), |  | ||||
|                 ('danceability', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('energy', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('loudness', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('speechiness', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('acousticness', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('instrumentalness', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('valence', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|                 ('tempo', models.DecimalField(decimal_places=2, max_digits=2)), |  | ||||
|             ], |  | ||||
|             options={ |  | ||||
|                 'verbose_name': 'AudioFeatures', |  | ||||
|                 'verbose_name_plural': 'AudioFeatures', |  | ||||
|             }, |  | ||||
|         ), |  | ||||
|         migrations.AddField( |  | ||||
|             model_name='track', |  | ||||
|             name='artist', |  | ||||
|             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spotifyvis.Artist'), |  | ||||
|         ), |  | ||||
|         migrations.AddField( |  | ||||
|             model_name='track', |  | ||||
|             name='users', |  | ||||
|             field=models.ManyToManyField(to='spotifyvis.User'), |  | ||||
|         ), |  | ||||
|         migrations.AlterUniqueTogether( |  | ||||
|             name='track', |  | ||||
|             unique_together={('track_id', 'artist')}, |  | ||||
|         ), |  | ||||
|     ] |  | ||||
| @ -0,0 +1,8 @@ | |||||
|  | body { | ||||
|  | background-color: #1e1e1e; | ||||
|  | } | ||||
|  | 
 | ||||
|  | h1,p { | ||||
|  | color: grey; | ||||
|  | } | ||||
|  | 
 | ||||
| @ -0,0 +1,137 @@ | |||||
|  | function create_genre_graph(data) { | ||||
|  |     // convert strings to nums {{{ //
 | ||||
|  |      | ||||
|  |     data.forEach(function(d) { | ||||
|  |         d.num_songs = +d.num_songs; | ||||
|  |         console.log(d.genre, d.num_songs); | ||||
|  |         var artist_names = Object.keys(d.artists); | ||||
|  |         artist_names.forEach(function(e) { | ||||
|  |             d.artists[e] = +d.artists[e]; | ||||
|  |             console.log(e, d.artists[e]); | ||||
|  |             //console.log(e, d.artists[e], d.artists[e] + 1);
 | ||||
|  |         }); | ||||
|  |     }); | ||||
|  |      | ||||
|  |     // }}} convert strings to nums //
 | ||||
|  | 
 | ||||
|  | 	// domains {{{ //
 | ||||
|  | 	 | ||||
|  |     data.sort(function(a, b) { | ||||
|  |         return b.num_songs - a.num_songs; | ||||
|  |     }); | ||||
|  |     x.domain(data.map(function(d) { | ||||
|  |         return d.genre; | ||||
|  |     })); | ||||
|  |     //y.domain([0, d3.max(data, function(d) { return d.num_songs; }) * 1.25]).nice();
 | ||||
|  |     y.domain([0, d3.max(data, function(d) { | ||||
|  |         return d.num_songs; | ||||
|  |     })]).nice(); | ||||
|  | 	 | ||||
|  | 	// }}} domains  //
 | ||||
|  | 
 | ||||
|  |     // setup bar colors {{{ //
 | ||||
|  |      | ||||
|  |     var max_artists = d3.max(data, function(d) { | ||||
|  |         return Object.keys(d.artists).length; | ||||
|  |     }); | ||||
|  |     var z = d3.scaleOrdinal().range(randomColor({ | ||||
|  |         count: max_artists, | ||||
|  |         luminosity: 'light', | ||||
|  |     })); | ||||
|  |      | ||||
|  |     // }}} setup bar colors //
 | ||||
|  | 
 | ||||
|  |     for (var genre_dict of data) { | ||||
|  | 		 | ||||
|  |         // process artist breakdown  {{{ //
 | ||||
|  |          | ||||
|  |         var keys = Object.keys(genre_dict.artists); | ||||
|  |         var stack = d3.stack() | ||||
|  |             //.order(d3.stackOrderAscending)
 | ||||
|  |             .order(d3.stackOrderDescending) | ||||
|  |             .keys(keys)([genre_dict.artists]) | ||||
|  |             //unpack the column
 | ||||
|  |             .map((d, i) => { | ||||
|  |                 return { | ||||
|  |                     key: keys[i], | ||||
|  |                     data: d[0] | ||||
|  |                 } | ||||
|  |             }); | ||||
|  |          | ||||
|  |         // }}} process artist breakdown  //
 | ||||
|  | 
 | ||||
|  |         // add bars {{{ //
 | ||||
|  |          | ||||
|  |         g.append("g") | ||||
|  |             .selectAll("rect") | ||||
|  |             .data(stack) | ||||
|  |             .enter().append("rect") | ||||
|  |             .attr("x", x(genre_dict.genre)) | ||||
|  |             .attr("y", function(d) { | ||||
|  |                 return y(d.data[1]); | ||||
|  |             }) | ||||
|  |             .attr("height", d => y(d.data[0]) - y(d.data[1])) | ||||
|  |             .attr("width", x.bandwidth()) | ||||
|  |             .attr('fill', (d, i) => z(i)) | ||||
|  |             .append('title').text(d => d.key + ': ' + (d.data[1] - d.data[0])); | ||||
|  |          | ||||
|  |         // }}} add bars //
 | ||||
|  | 
 | ||||
|  |         // x-axis {{{ //
 | ||||
|  |          | ||||
|  |         g.append("g") | ||||
|  |             .attr("class", "axis") | ||||
|  |             .attr("transform", "translate(0," + height + ")") | ||||
|  |             .call(d3.axisBottom(x)) | ||||
|  |             .selectAll(".tick text") | ||||
|  |             .call(wrap, x.bandwidth()); | ||||
|  |          | ||||
|  |         // }}} x-axis //
 | ||||
|  | 
 | ||||
|  |         // y-axis {{{ //
 | ||||
|  |          | ||||
|  |         g.append("g") | ||||
|  |             .attr("class", "axis") | ||||
|  |             .call(d3.axisLeft(y).ticks(null, "s")) | ||||
|  |             .append("text") | ||||
|  |             .attr("x", 2) | ||||
|  |             .attr("y", y(y.ticks().pop()) + 0.5) | ||||
|  |             .attr("dy", "0.32em") | ||||
|  |             .attr("fill", "#000") | ||||
|  |             .attr("font-weight", "bold") | ||||
|  |             .attr("text-anchor", "start") | ||||
|  |             .text("Songs"); | ||||
|  |          | ||||
|  |         // }}} y-axis //
 | ||||
|  | 
 | ||||
|  |     } | ||||
|  | } | ||||
|  | 
 | ||||
|  | // wrap text {{{ //
 | ||||
|  | 
 | ||||
|  | // https://gist.github.com/guypursey/f47d8cd11a8ff24854305505dbbd8c07#file-index-html
 | ||||
|  | function wrap(text, width) { | ||||
|  |     text.each(function() { | ||||
|  |         var text = d3.select(this), | ||||
|  | 			words = text.text().split(/\s+/).reverse(), | ||||
|  |             word, | ||||
|  |             line = [], | ||||
|  |             lineNumber = 0, | ||||
|  |             lineHeight = 1.1, // ems
 | ||||
|  |             y = text.attr("y"), | ||||
|  |             dy = parseFloat(text.attr("dy")), | ||||
|  |             tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em") | ||||
|  |         while (word = words.pop()) { | ||||
|  |             line.push(word) | ||||
|  |             tspan.text(line.join(" ")) | ||||
|  |             if (tspan.node().getComputedTextLength() > width) { | ||||
|  |                 line.pop() | ||||
|  |                 tspan.text(line.join(" ")) | ||||
|  |                 line = [word] | ||||
|  |                 tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", `${++lineNumber * lineHeight + dy}em`).text(word) | ||||
|  |             } | ||||
|  |         } | ||||
|  |     }) | ||||
|  | } | ||||
|  | 
 | ||||
|  | // }}} wrap text //
 | ||||
| @ -0,0 +1,141 @@ | |||||
|  | {% load static %} | ||||
|  | <!DOCTYPE html> | ||||
|  | <!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> | ||||
|  | <!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]--> | ||||
|  | <!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]--> | ||||
|  | <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--> | ||||
|  |     <head> | ||||
|  |         <meta charset="utf-8"> | ||||
|  |         <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
|  |         <title>User Spotify Data</title> | ||||
|  |         <meta name="description" content=""> | ||||
|  |         <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|  |         <style> | ||||
|  |             .tick { | ||||
|  |                 font-size: 15px; | ||||
|  |             } | ||||
|  |         </style> | ||||
|  |     </head> | ||||
|  |     <body> | ||||
|  |         <!--[if lt IE 7]> | ||||
|  |             <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p> | ||||
|  |         <![endif]--> | ||||
|  |         <p>Logged in as {{ user_id }}</p> | ||||
|  |         <script src="https://d3js.org/d3.v5.js"></script> | ||||
|  |         <script type="text/javascript"> | ||||
|  | 
 | ||||
|  |             /** Queries the backend for audio feature data, draws the bar chart | ||||
|  |              *  illustrating the frequencies of values, and appends the chart to | ||||
|  |              *  a designated parent element | ||||
|  |              * | ||||
|  |              *  @param audioFeature: the name of the audio feature (string) | ||||
|  |              *  @param intervalEndPoints: a sorted array of 5 real numbers defining the intervals (categories) of values, | ||||
|  |              *  for example: | ||||
|  |              *      [0, 0.25, 0.5, 0.75, 1.0] for instrumentalness would define ranges | ||||
|  |              *      (0-0.25), (0.25-0.5), (0.5-0.75), (0.75-1.0) | ||||
|  |              *  @param parentElem: the DOM element to append the graph to (a selector string) | ||||
|  |              *  @return None | ||||
|  |              */ | ||||
|  |             function drawAudioFeatGraph(audioFeature, intervalEndPoints, parentElem) { | ||||
|  |                 let margin = {top: 20, right: 30, bottom: 30, left: 40}; | ||||
|  |                 let width = 480 - margin.left - margin.right, | ||||
|  |                     height = 270 - margin.top - margin.bottom; | ||||
|  | 
 | ||||
|  |                 let featureData = {}; | ||||
|  |                 // Create the keys first in order | ||||
|  |                 for (let index = 0; index < intervalEndPoints.length - 1; index++) { | ||||
|  |                     let key = `${intervalEndPoints[index]} ~ ${intervalEndPoints[index + 1]}`; | ||||
|  |                     featureData[key] = 0; | ||||
|  |                 } | ||||
|  |                 // define the vertical scaling function | ||||
|  |                 let vScale = d3.scaleLinear().range([height, 0]); | ||||
|  | 
 | ||||
|  |                 d3.json(`/audio_features/${audioFeature}/{{ user_secret }}`) | ||||
|  |                     .then(function(response) { | ||||
|  |                     // categorize the data points | ||||
|  |                     for (let dataPoint of response.data_points) { | ||||
|  |                         dataPoint = parseFloat(dataPoint); | ||||
|  |                         let index = intervalEndPoints.length - 2; | ||||
|  |                         // find the index of the first element greater than dataPoint | ||||
|  |                         while (dataPoint < intervalEndPoints[index]) { | ||||
|  |                             index -= 1; | ||||
|  |                         } | ||||
|  |                         let key = `${intervalEndPoints[index]} ~ ${intervalEndPoints[index + 1]}`; | ||||
|  |                         featureData[key] += 1; | ||||
|  |                     } | ||||
|  | 
 | ||||
|  |                     let dataSet = Object.values(featureData); | ||||
|  |                     let dataRanges = Object.keys(featureData); // Ranges of audio features, e.g. 0-0.25, 0.25-0.5, etc | ||||
|  |                     let dataArr = []; | ||||
|  |                     // turn the counts into an array of objects, e.g. {range: "0-0.25", counts: 5} | ||||
|  |                     for (let i = 0; i < dataRanges.length; i++) { | ||||
|  |                         dataArr.push({ | ||||
|  |                             range: dataRanges[i], | ||||
|  |                             counts: featureData[dataRanges[i]] | ||||
|  |                         }); | ||||
|  |                     } | ||||
|  |                     vScale.domain([0, d3.max(dataSet)]).nice(); | ||||
|  | 
 | ||||
|  |                     let hScale = d3.scaleBand().domain(dataRanges).rangeRound([0, width]).padding(0.5); | ||||
|  | 
 | ||||
|  |                     let xAxis = d3.axisBottom().scale(hScale); | ||||
|  |                     let yAxis = d3.axisLeft().scale(vScale); | ||||
|  | 
 | ||||
|  |                     let featureSVG = d3.select(parentElem) | ||||
|  |                         .append('svg').attr('width', width + margin.left + margin.right) | ||||
|  |                         .attr('height', height + margin.top + margin.bottom); | ||||
|  | 
 | ||||
|  |                     let featureGraph = featureSVG.append("g") | ||||
|  |                         .attr("transform", `translate(${margin.left}, ${margin.top})`) | ||||
|  |                         .attr("fill", "teal"); | ||||
|  | 
 | ||||
|  |                     featureGraph.selectAll(".bar") | ||||
|  |                         .data(dataArr) | ||||
|  |                         .enter().append('rect') | ||||
|  |                         .attr('class', 'bar') | ||||
|  |                         .attr('x', function(d) { return hScale(d.range); }) | ||||
|  |                         .attr('y', function(d) { return vScale(d.counts); }) | ||||
|  |                         .attr("height", function(d) { return height - vScale(d.counts); }) | ||||
|  |                         .attr("width", hScale.bandwidth()); | ||||
|  | 
 | ||||
|  |                     // function(d) { return hScale(d.range); } | ||||
|  | 
 | ||||
|  |                     featureGraph.append('g') | ||||
|  |                         .attr('class', 'axis') | ||||
|  |                         .attr('transform', `translate(0, ${height})`) | ||||
|  |                         .call(xAxis); | ||||
|  | 
 | ||||
|  |                     featureGraph.append('g') | ||||
|  |                         .attr('class', 'axis') | ||||
|  |                         .call(yAxis); | ||||
|  | 
 | ||||
|  |                     featureSVG.append("text") | ||||
|  |                         .attr('x', (width / 2)) | ||||
|  |                         .attr('y', (margin.top / 2)) | ||||
|  |                         .attr('text-anchor', 'middle') | ||||
|  |                         .style('font-size', '14px') | ||||
|  |                         .text(`${capFeatureStr(audioFeature)}`); | ||||
|  | 
 | ||||
|  |                 }); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             /** | ||||
|  |              * Returns the audio feature name string with the first letter capitalized | ||||
|  |              * @param audioFeature: the name of the audio feature | ||||
|  |              * @returns the audio feature name string with the first letter capitalized | ||||
|  |              */ | ||||
|  |             function capFeatureStr(audioFeature) { | ||||
|  |                 return audioFeature.charAt(0).toUpperCase() + audioFeature.slice(1); | ||||
|  |             } | ||||
|  | 
 | ||||
|  |             drawAudioFeatGraph("instrumentalness", [0, 0.25, 0.5, 0.75, 1.0], 'body'); | ||||
|  |             drawAudioFeatGraph("valence", [0, 0.25, 0.5, 0.75, 1.0], 'body'); | ||||
|  |             drawAudioFeatGraph("energy", [0, 0.25, 0.5, 0.75, 1.0], 'body'); | ||||
|  |             drawAudioFeatGraph("tempo", [40, 80, 120, 160, 200], 'body'); | ||||
|  |             drawAudioFeatGraph("danceability", [0, 0.25, 0.5, 0.75, 1.0], 'body'); | ||||
|  |             drawAudioFeatGraph("acousticness", [0, 0.25, 0.5, 0.75, 1.0], 'body'); | ||||
|  |             drawAudioFeatGraph("loudness", [-60, -45, -30, -15, 0], 'body'); | ||||
|  |             drawAudioFeatGraph("speechiness", [0, 0.25, 0.5, 0.75, 1.0], 'body'); | ||||
|  |         </script> | ||||
|  |     </body> | ||||
|  | </html> | ||||
| @ -0,0 +1,44 @@ | |||||
|  | <!-- header {{{ --> | ||||
|  | 
 | ||||
|  | <!DOC | ||||
|  | <!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> | ||||
|  | <!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]--> | ||||
|  | <!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]--> | ||||
|  | 	<!--[if gt IE 8]><!-->  | ||||
|  | {% load static %} | ||||
|  | <html class="no-js"> <!--<![endif]--> | ||||
|  | <head> | ||||
|  | 	<meta charset="utf-8"> | ||||
|  | 	<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
|  | 	<title>Test DB Page</title> | ||||
|  | 	<meta name="description" content=""> | ||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|  |     <link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}"> | ||||
|  | </head> | ||||
|  | 
 | ||||
|  | <!-- }}} header --> | ||||
|  | 
 | ||||
|  | <body> | ||||
|  |   <script src="https://d3js.org/d3.v5.min.js"></script> | ||||
|  |   <script src="https://cdnjs.cloudflare.com/ajax/libs/randomcolor/0.5.2/randomColor.min.js"></script> | ||||
|  |   {% load static %} | ||||
|  |   <script src="{% static "spotifyvis/scripts/genre_graph.js" %}"></script> | ||||
|  | 
 | ||||
|  |   <svg width="1920" height="740"></svg> | ||||
|  |   <script> | ||||
|  |     var svg = d3.select("svg"), | ||||
|  |         margin = {top: 20, right: 20, bottom: 30, left: 40}, | ||||
|  |         width = +svg.attr("width") - margin.left - margin.right, | ||||
|  |         height = +svg.attr("height") - margin.top - margin.bottom, | ||||
|  |         g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | ||||
|  |     var x = d3.scaleBand() | ||||
|  |         .rangeRound([0, width]) | ||||
|  |         .paddingInner(0.05) | ||||
|  |         .align(0.1); | ||||
|  |     var y = d3.scaleLinear() | ||||
|  |         .rangeRound([height, 0]); | ||||
|  | 
 | ||||
|  |     d3.json("{% url "get_genre_data" user_secret %}").then(create_genre_graph); | ||||
|  |   </script> | ||||
|  | </body> | ||||
|  | </html> | ||||
| @ -0,0 +1,17 @@ | |||||
|  | <!DOCTYPE html> | ||||
|  | {% load static %} | ||||
|  | <html lang="en"> | ||||
|  | <head> | ||||
|  |     <meta charset="UTF-8"> | ||||
|  |     <title>Logged In</title> | ||||
|  |     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> | ||||
|  |     <link rel="stylesheet" href="{% static 'spotifyvis/css/dark_bg.css' %}"> | ||||
|  | </head> | ||||
|  | <body> | ||||
|  | 	<h1>{{ user_id }}'s Graphs</h1> | ||||
|  | 	<a class="btn btn-primary" href="/audio_features/{{ user_secret }}" | ||||
|  | 		role="button">Audio Features</a> | ||||
|  |     <a class="btn btn-primary" href="{% url "display_genre_graph" user_secret %}" | ||||
|  | 		role="button">Genres</a> | ||||
|  | </body> | ||||
|  | </html> | ||||
| @ -1,9 +1,19 @@ | |||||
| from django.urls import path, include | from django.urls import path, include | ||||
| from . import views |  | ||||
|  | from django.conf.urls import url | ||||
|  | 
 | ||||
|  | from .views import * | ||||
| 
 | 
 | ||||
| urlpatterns = [ | urlpatterns = [ | ||||
|     path('', views.index, name='index'), |  | ||||
|     path('login', views.login, name='login'), |  | ||||
|     path('callback', views.callback, name='callback'), |  | ||||
|     path('user_data', views.user_data, name='user_data'), |  | ||||
|  |     path('', index, name='index'), | ||||
|  |     path('login', login, name='login'), | ||||
|  |     path('callback', callback, name='callback'), | ||||
|  |     path('user_data', user_data, name='user_data'), | ||||
|  |     path('admin_graphs', admin_graphs, name='admin_graphs'), | ||||
|  |     path('user_artists/<str:user_id>', get_artist_data, name='get_artist_data'), | ||||
|  |     path('api/user_genres/<str:user_secret>', get_genre_data, name='get_genre_data'), | ||||
|  |     path('graphs/genre/<str:client_secret>', display_genre_graph, | ||||
|  |         name='display_genre_graph'), | ||||
|  |     path('audio_features/<str:client_secret>', audio_features, name='audio_features'), | ||||
|  |     path('audio_features/<str:audio_feature>/<str:client_secret>', | ||||
|  |         get_audio_feature_data, name='get_audio_feature_data'), | ||||
| ] | ] | ||||
| @ -1,300 +1,281 @@ | |||||
|  | #  imports {{{ #  | ||||
| import requests | import requests | ||||
| import math | import math | ||||
| import pprint | import pprint | ||||
| from .models import Artist, User, Track, AudioFeatures |  | ||||
|  | 
 | ||||
|  | from .models import * | ||||
|  | from django.db.models import Count, Q, F | ||||
|  | from django.http import JsonResponse | ||||
|  | from django.core import serializers | ||||
|  | import json | ||||
|  | 
 | ||||
|  | #  }}} imports #  | ||||
|  | 
 | ||||
|  | USER_TRACKS_LIMIT = 50 | ||||
|  | ARTIST_LIMIT = 50 | ||||
|  | FEATURES_LIMIT = 100 | ||||
|  | #  ARTIST_LIMIT = 25 | ||||
|  | #  FEATURES_LIMIT = 25 | ||||
| 
 | 
 | ||||
| #  parse_library {{{ #  | #  parse_library {{{ #  | ||||
| 
 | 
 | ||||
| def parse_library(headers, tracks, library_stats, user): |  | ||||
|     """Scans user's library for certain number of tracks to update library_stats with. |  | ||||
|  | def parse_library(headers, tracks, user): | ||||
|  |     """Scans user's library for certain number of tracks and store the information in a database | ||||
| 
 | 
 | ||||
|     :headers: For API call. |     :headers: For API call. | ||||
|     :tracks: Number of tracks to get from user's library. |     :tracks: Number of tracks to get from user's library. | ||||
|     :library_stats: Dictionary containing the data mined from user's library  |  | ||||
|     :user: a User object representing the user whose library we are parsing |     :user: a User object representing the user whose library we are parsing | ||||
| 
 | 
 | ||||
|     :returns: None |     :returns: None | ||||
| 
 | 
 | ||||
|     """ |     """ | ||||
|     #  TODO: implement importing entire library with 0 as tracks param |     #  TODO: implement importing entire library with 0 as tracks param | ||||
|     # number of tracks to get with each call |  | ||||
|     limit = 5 |  | ||||
|     # keeps track of point to get songs from |     # keeps track of point to get songs from | ||||
|     offset = 0 |     offset = 0 | ||||
|     payload = {'limit': str(limit)} |  | ||||
|     # use two separate variables to track, because the average popularity also requires num_samples  |  | ||||
|     num_samples = 0  # number of actual track samples |  | ||||
|     feature_data_points = 0  # number of feature data analyses (some tracks do not have analyses available) |  | ||||
|  |     payload = {'limit': str(USER_TRACKS_LIMIT)} | ||||
|  |     artist_genre_queue = [] | ||||
|  |     features_queue = [] | ||||
| 
 | 
 | ||||
|     for _ in range(0, tracks, limit): |  | ||||
|  |     # iterate until hit requested num of tracks | ||||
|  |     for i in range(0, tracks, USER_TRACKS_LIMIT): | ||||
|         payload['offset'] = str(offset) |         payload['offset'] = str(offset) | ||||
|         saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks', headers=headers, params=payload).json() |  | ||||
|         for track_dict in saved_tracks_response['items']: |  | ||||
|             num_samples += 1  |  | ||||
|             get_track_info(track_dict['track'], library_stats, num_samples) |  | ||||
|             #  get_genre(headers, track_dict['track']['album']['id']) |  | ||||
|             audio_features_dict = get_audio_features(headers, track_dict['track']['id']) |  | ||||
|             if len(audio_features_dict) != 0: |  | ||||
|                 # Track the number of audio analyses for calculating |  | ||||
|                 # audio feature averages and standard deviations on the fly |  | ||||
|                 feature_data_points += 1 |  | ||||
|                  |  | ||||
|                 for feature, feature_data in audio_features_dict.items(): |  | ||||
|                     update_audio_feature_stats(feature, feature_data, feature_data_points, library_stats) |  | ||||
|             for artist_dict in track_dict['track']['artists']: |  | ||||
|                 increase_artist_count(headers, artist_dict['name'], artist_dict['id'], library_stats) |  | ||||
|         # calculates num_songs with offset + songs retrieved |  | ||||
|         library_stats['num_songs'] = offset + len(saved_tracks_response['items']) |  | ||||
|         offset += limit |  | ||||
|     calculate_genres_from_artists(headers, library_stats) |  | ||||
|     pprint.pprint(library_stats) |  | ||||
|  |         saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',  | ||||
|  |                 headers=headers, | ||||
|  |                 params=payload).json() | ||||
| 
 | 
 | ||||
| #  }}} parse_library #  |  | ||||
| 
 |  | ||||
| def get_audio_features(headers, track_id): |  | ||||
|     """Returns the audio features of a soundtrack |  | ||||
| 
 |  | ||||
|     Args: |  | ||||
|         headers: headers containing the API token |  | ||||
|         track_id: the id of the soundtrack, needed to query the Spotify API |  | ||||
|          |  | ||||
|     Returns: |  | ||||
|         A dictionary with the features as its keys, if audio feature data is missing for the track,  |  | ||||
|         an empty dictionary is returned. |  | ||||
|     """ |  | ||||
|  |         for track_dict in saved_tracks_response['items']: | ||||
|  |             #  add artists {{{ #  | ||||
|              |              | ||||
|     response = requests.get("https://api.spotify.com/v1/audio-features/{}".format(track_id), headers = headers).json() |  | ||||
|     if 'error' in response: |  | ||||
|         return {} |  | ||||
|     features_dict = {} |  | ||||
|  |             # update artist info before track so that Track object can reference | ||||
|  |             # Artist object | ||||
|  |             track_artists = [] | ||||
|  |             for artist_dict in track_dict['track']['artists']: | ||||
|  |                 artist_obj, artist_created = Artist.objects.get_or_create( | ||||
|  |                         artist_id=artist_dict['id'], | ||||
|  |                         name=artist_dict['name'],) | ||||
|  |                 # only add/tally up artist genres if new | ||||
|  |                 if artist_created: | ||||
|  |                     artist_genre_queue.append(artist_obj) | ||||
|  |                     if len(artist_genre_queue) == ARTIST_LIMIT: | ||||
|  |                         add_artist_genres(headers, artist_genre_queue) | ||||
|  |                         artist_genre_queue = [] | ||||
|  |                 track_artists.append(artist_obj) | ||||
|  |              | ||||
|  |             #  }}} add artists #  | ||||
|  |              | ||||
|  |             # TODO: fix this, don't need any more | ||||
|  |             top_genre = "" | ||||
|  |             track_obj, track_created = save_track_obj(track_dict['track'],  | ||||
|  |                     track_artists, top_genre, user) | ||||
|  | 
 | ||||
|  |             #  add audio features {{{ #  | ||||
|  |              | ||||
|  |             # if a new track is not created, the associated audio feature does | ||||
|  |             # not need to be created again | ||||
|  |             if track_created: | ||||
|  |                 features_queue.append(track_obj) | ||||
|  |                 if len(features_queue) == FEATURES_LIMIT: | ||||
|  |                     get_audio_features(headers, features_queue) | ||||
|  |                     features_queue = [] | ||||
|  |              | ||||
|  |             #  }}} add audio features #  | ||||
|  | 
 | ||||
|  |             # temporary console logging | ||||
|  |             print("#{}-{}: {} - {}".format(offset + 1, | ||||
|  |                 offset + USER_TRACKS_LIMIT,  | ||||
|  |                 track_obj.artists.first(),  | ||||
|  |                 track_obj.name)) | ||||
| 
 | 
 | ||||
|     # Data that we don't need |  | ||||
|     useless_keys = [  |  | ||||
|         "key", "mode", "type", "liveness", "id", "uri", "track_href", "analysis_url", "time_signature", |  | ||||
|     ] |  | ||||
|     for key, val in response.items(): |  | ||||
|         if key not in useless_keys: |  | ||||
|             features_dict[key] = val |  | ||||
|  |         # calculates num_songs with offset + songs retrieved | ||||
|  |         offset += USER_TRACKS_LIMIT | ||||
| 
 | 
 | ||||
|     return features_dict |  | ||||
|  |     #  clean-up {{{ #  | ||||
|      |      | ||||
|  |     # update remaining artists without genres and songs without features if | ||||
|  |     # there are any | ||||
|  |     if len(artist_genre_queue) > 0: | ||||
|  |         add_artist_genres(headers, artist_genre_queue) | ||||
|  |     if len(features_queue) > 0: | ||||
|  |         get_audio_features(headers, features_queue) | ||||
|      |      | ||||
| def update_std_dev(cur_mean, cur_std_dev, new_data_point, sample_size): |  | ||||
|     """Calculates the standard deviation for a sample without storing all data points |  | ||||
|  |     #  }}} clean-up #  | ||||
| 
 | 
 | ||||
|     Args: |  | ||||
|         cur_mean: the current mean for N = (sample_size - 1) |  | ||||
|         cur_std_dev: the current standard deviation for N = (sample_size - 1) |  | ||||
|         new_data_point: a new data point |  | ||||
|         sample_size: sample size including the new data point |  | ||||
|  |     update_track_genres(user) | ||||
| 
 | 
 | ||||
|     Returns: |  | ||||
|         (new_mean, new_std_dev) |  | ||||
|     """ |  | ||||
|     # This is an implementation of Welford's method |  | ||||
|     # http://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/ |  | ||||
|     new_mean = ((sample_size - 1) * cur_mean + new_data_point) / sample_size |  | ||||
|     delta_variance = (new_data_point - new_mean) * (new_data_point - cur_mean) |  | ||||
|     new_std_dev = math.sqrt( |  | ||||
|         (math.pow(cur_std_dev, 2) * (sample_size - 2) + delta_variance) / ( |  | ||||
|         sample_size - 1 |  | ||||
|     )) |  | ||||
|     return new_mean, new_std_dev |  | ||||
|  | #  }}} parse_library #  | ||||
| 
 | 
 | ||||
|  | #  update_track_genres {{{ #  | ||||
| 
 | 
 | ||||
| def update_audio_feature_stats(feature, new_data_point, sample_size, library_stats): |  | ||||
|     """Updates the audio feature statistics in library_stats |  | ||||
|  | def update_track_genres(user): | ||||
|  |     """Updates user's tracks with the most common genre associated with the | ||||
|  |     songs' artist(s). | ||||
| 
 | 
 | ||||
|     Args: |  | ||||
|         feature: the audio feature to be updated (string) |  | ||||
|         new_data_point: new data to update the stats with |  | ||||
|         sample_size: sample size including the new data point |  | ||||
|         library_stats Dictionary containing the data mined from user's Spotify library |  | ||||
|  |     :user: User object who's tracks are being updated. | ||||
| 
 | 
 | ||||
|  |     :returns: None | ||||
| 
 | 
 | ||||
|     Returns: |  | ||||
|         None |  | ||||
|     """ |     """ | ||||
|     # first time the feature is considered |  | ||||
|     if sample_size < 2: |  | ||||
|         library_stats['audio_features'][feature] = { |  | ||||
|             "average": new_data_point, |  | ||||
|             "std_dev": 0, |  | ||||
|         } |  | ||||
|     else: |  | ||||
|         cur_mean = library_stats['audio_features'][feature]['average'] |  | ||||
|         cur_std_dev = library_stats['audio_features'][feature]['std_dev'] |  | ||||
|         new_mean, new_std_dev = update_std_dev(cur_mean, cur_std_dev, new_data_point, sample_size) |  | ||||
| 
 |  | ||||
|         library_stats['audio_features'][feature] = { |  | ||||
|             "average": new_mean, |  | ||||
|             "std_dev": new_std_dev |  | ||||
|         } |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| #  increase_nested_key {{{ #  |  | ||||
| 
 |  | ||||
| def increase_nested_key(top_key, nested_key, library_stats, amount=1): |  | ||||
|     """Increases count for the value of library_stats[top_key][nested_key]. Checks if nested_key exists already and takes |  | ||||
|     appropriate action. |  | ||||
| 
 |  | ||||
|     :top_key: First key of library_stats. |  | ||||
|     :nested_key: Key in top_key's dict for which we want to increase value of. |  | ||||
|     :library_stats: Dictionary containing the data mined from user's Spotify library |  | ||||
| 
 |  | ||||
|     :returns: None |  | ||||
|  |     user_tracks = Track.objects.filter(users__exact=user) | ||||
|  |     for track in user_tracks: | ||||
|  |         # just using this variable to save another call to db | ||||
|  |         track_artists = track.artists.all() | ||||
|  |         # set genres to first artist's genres then find intersection with others | ||||
|  |         shared_genres = track_artists.first().genres.all() | ||||
|  |         for artist in track_artists: | ||||
|  |             shared_genres = shared_genres.intersection(artist.genres.all()) | ||||
|  |         shared_genres = shared_genres.order_by('-num_songs') | ||||
|  | 
 | ||||
|  |         undefined_genre_obj = Genre.objects.get(name="undefined") | ||||
|  |         most_common_genre = shared_genres.first() if shared_genres.first() is \ | ||||
|  |                 not undefined_genre_obj else shared_genres[1] | ||||
|  |         track.genre = most_common_genre if most_common_genre is not None \ | ||||
|  |                 else undefined_genre_obj | ||||
|  |         track.save() | ||||
|  |         #  print(track.name, track.genre) | ||||
|  | 
 | ||||
|  | #  }}}  update_track_genres #  | ||||
|  | 
 | ||||
|  | #  save_track_obj {{{ #  | ||||
|  | 
 | ||||
|  | def save_track_obj(track_dict, artists, top_genre, user): | ||||
|  |     """Make an entry in the database for this track if it doesn't exist already. | ||||
|  | 
 | ||||
|  |     :track_dict: dictionary from the API call containing track information. | ||||
|  |     :artists: artists of the song, passed in as a list of Artist objects. | ||||
|  |     :top_genre: top genre associated with this track (see get_top_genre). | ||||
|  |     :user: User object for which this Track is to be associated with. | ||||
|  | 
 | ||||
|  |     :returns: (The created/retrieved Track object, created)  | ||||
| 
 | 
 | ||||
|     """ |     """ | ||||
|     if nested_key not in library_stats[top_key]: |  | ||||
|         library_stats[top_key][nested_key] = amount |  | ||||
|  |     track_query = Track.objects.filter(track_id__exact=track_dict['id']) | ||||
|  |     if len(track_query) != 0: | ||||
|  |         return track_query[0], False | ||||
|     else: |     else: | ||||
|         library_stats[top_key][nested_key] += amount |  | ||||
| 
 |  | ||||
| #  }}} increase_nested_key #  |  | ||||
| 
 |  | ||||
| #  increase_artist_count {{{ #  |  | ||||
| 
 |  | ||||
| def increase_artist_count(headers, artist_name, artist_id, library_stats): |  | ||||
|     """Increases count for artist in library_stats and stores the artist_id.  |  | ||||
| 
 |  | ||||
|     :headers: For making the API call. |  | ||||
|     :artist_name: Artist to increase count for. |  | ||||
|     :artist_id: The Spotify ID for the artist. |  | ||||
|     :library_stats: Dictionary containing the data mined from user's Spotify library |  | ||||
|  |         new_track = Track.objects.create( | ||||
|  |             track_id=track_dict['id'], | ||||
|  |             year=track_dict['album']['release_date'].split('-')[0], | ||||
|  |             popularity=int(track_dict['popularity']), | ||||
|  |             runtime=int(float(track_dict['duration_ms']) / 1000), | ||||
|  |             name=track_dict['name'], | ||||
|  |             #  genre=top_genre, | ||||
|  |             ) | ||||
|  | 
 | ||||
|  |         # have to add artists and user after saving object since track needs to | ||||
|  |         # have ID before filling in m2m field | ||||
|  |         for artist in artists: | ||||
|  |             new_track.artists.add(artist) | ||||
|  |         new_track.users.add(user) | ||||
|  |         new_track.save() | ||||
|  |         return new_track, True | ||||
|  | 
 | ||||
|  | #  }}} save_track_obj #  | ||||
|  | 
 | ||||
|  | #  get_audio_features {{{ #  | ||||
|  | 
 | ||||
|  | def get_audio_features(headers, track_objs): | ||||
|  |     """Creates and saves a new AudioFeatures objects for the respective | ||||
|  |     track_objs. track_objs should contain the API limit for a single call | ||||
|  |     (FEATURES_LIMIT) for maximum efficiency. | ||||
|  | 
 | ||||
|  |     :headers: headers containing the API token | ||||
|  |     :track_objs: Track objects to associate with the new AudioFeatures object | ||||
|          |          | ||||
|     :returns: None |     :returns: None | ||||
| 
 |  | ||||
|     """ |     """ | ||||
|     if artist_name not in library_stats['artists']: |  | ||||
|         library_stats['artists'][artist_name] = {} |  | ||||
|         library_stats['artists'][artist_name]['count'] = 1 |  | ||||
|         library_stats['artists'][artist_name]['id'] = artist_id |  | ||||
|     else: |  | ||||
|         library_stats['artists'][artist_name]['count'] += 1 |  | ||||
| 
 |  | ||||
| #  }}} increase_artist_count #  |  | ||||
| 
 |  | ||||
| def update_popularity_stats(new_data_point, library_stats, sample_size): |  | ||||
|     """Updates the popularity statistics in library_stats |  | ||||
|  |     track_ids = str.join(",", [track_obj.track_id for track_obj in track_objs]) | ||||
|  |     params = {'ids': track_ids} | ||||
|  |     features_response = requests.get("https://api.spotify.com/v1/audio-features", | ||||
|  |             headers=headers,params=params).json()['audio_features'] | ||||
|  |     #  pprint.pprint(features_response) | ||||
|  | 
 | ||||
|  |     useless_keys = [ "key", "mode", "type", "liveness", "id", "uri", "track_href", "analysis_url", "time_signature", ] | ||||
|  |     for i in range(len(track_objs)): | ||||
|  |         if features_response[i] is not None: | ||||
|  |             # Data that we don't need | ||||
|  |             cur_features_obj = AudioFeatures() | ||||
|  |             cur_features_obj.track = track_objs[i] | ||||
|  |             for key, val in features_response[i].items(): | ||||
|  |                 if key not in useless_keys: | ||||
|  |                     setattr(cur_features_obj, key, val) | ||||
|  |             cur_features_obj.save() | ||||
| 
 | 
 | ||||
|     Args: |  | ||||
|         new_data_point: new data to update the popularity stats with |  | ||||
|         library_stats: Dictionary containing data mined from user's Spotify library |  | ||||
|         sample_size: The sample size including the new data |  | ||||
|  | #  }}} get_audio_features #  | ||||
| 
 | 
 | ||||
|     Returns: |  | ||||
|         None |  | ||||
|     """ |  | ||||
|     if sample_size < 2: |  | ||||
|         library_stats['popularity'] = { |  | ||||
|             "average": new_data_point, |  | ||||
|             "std_dev": 0, |  | ||||
|         } |  | ||||
|     else : |  | ||||
|         cur_mean_popularity = library_stats['popularity']['average'] |  | ||||
|         cur_popularity_stdev = library_stats['popularity']['std_dev'] |  | ||||
|         new_mean, new_std_dev = update_std_dev( |  | ||||
|             cur_mean_popularity, cur_popularity_stdev, new_data_point, sample_size) |  | ||||
|         library_stats['popularity'] = { |  | ||||
|             "average": new_mean, |  | ||||
|             "std_dev": new_std_dev, |  | ||||
|         } |  | ||||
| 
 |  | ||||
| #  get_track_info {{{ #  |  | ||||
| 
 |  | ||||
| def get_track_info(track_dict, library_stats, sample_size): |  | ||||
|     """Get all the info from the track_dict directly returned by the API call in parse_library. |  | ||||
| 
 |  | ||||
|     :track_dict: Dict returned from the API call containing the track info. |  | ||||
|     :library_stats: Dictionary containing the data mined from user's Spotify library |  | ||||
|     :sample_size: The sample size so far including this track |  | ||||
|  | def process_artist_genre(genre_name, artist_obj): | ||||
|  |     """Increase count for correspoding Genre object to genre_name and add that | ||||
|  |     Genre to artist_obj.  | ||||
| 
 | 
 | ||||
|  |     :genre_name: Name of genre. | ||||
|  |     :artist_obj: Artist object to add Genre object to. | ||||
|     :returns: None |     :returns: None | ||||
| 
 | 
 | ||||
|     """ |     """ | ||||
|     # popularity |  | ||||
|     update_popularity_stats(track_dict['popularity'], library_stats, sample_size) |  | ||||
|          |  | ||||
|     # year |  | ||||
|     year_released = track_dict['album']['release_date'].split('-')[0] |  | ||||
|     increase_nested_key('year_released', year_released, library_stats) |  | ||||
|      |  | ||||
|     # artist |  | ||||
|     #  artist_names = [artist['name'] for artist in track_dict['artists']] |  | ||||
|     #  for artist_name in artist_names: |  | ||||
|         #  increase_nested_key('artists', artist_name) |  | ||||
|  |     genre_obj, created = Genre.objects.get_or_create(name=genre_name, | ||||
|  |             defaults={'num_songs':1}) | ||||
|  |     if not created: | ||||
|  |         genre_obj.num_songs = F('num_songs') + 1 | ||||
|  |         genre_obj.save() | ||||
|  |     artist_obj.genres.add(genre_obj) | ||||
|  |     artist_obj.save() | ||||
| 
 | 
 | ||||
|     # runtime |  | ||||
|     library_stats['total_runtime'] += float(track_dict['duration_ms']) / (1000 * 60) |  | ||||
|  | #  add_artist_genres {{{ #  | ||||
| 
 | 
 | ||||
| #  }}} get_track_info #  |  | ||||
| 
 |  | ||||
| #  calculate_genres_from_artists {{{ #  |  | ||||
| 
 |  | ||||
| def calculate_genres_from_artists(headers, library_stats): |  | ||||
|     """Tallies up genre counts based on artists in library_stats. |  | ||||
|  | def add_artist_genres(headers, artist_objs): | ||||
|  |     """Adds genres to artist_objs and increases the count the respective Genre | ||||
|  |     object. artist_objs should contain the API limit for a single call | ||||
|  |     (ARTIST_LIMIT) for maximum efficiency. | ||||
| 
 | 
 | ||||
|     :headers: For making the API call. |     :headers: For making the API call. | ||||
|     :library_stats: Dictionary containing the data mined from user's Spotify library |  | ||||
|  |     :artist_objs: List of Artist objects for which to add/tally up genres for. | ||||
| 
 | 
 | ||||
|     :returns: None |     :returns: None | ||||
| 
 | 
 | ||||
|     """ |     """ | ||||
|     for artist_entry in library_stats['artists'].values(): |  | ||||
|         artist_response = requests.get('https://api.spotify.com/v1/artists/' + artist_entry['id'], headers=headers).json() |  | ||||
|         # increase each genre count by artist count |  | ||||
|         for genre in artist_response['genres']: |  | ||||
|             increase_nested_key('genres', genre, library_stats, artist_entry['count']) |  | ||||
|  |     artist_ids = str.join(",", [artist_obj.artist_id for artist_obj in artist_objs]) | ||||
|  |     params = {'ids': artist_ids} | ||||
|  |     artists_response = requests.get('https://api.spotify.com/v1/artists/', | ||||
|  |             headers=headers, params=params).json()['artists'] | ||||
|  |     #  pprint.pprint(artists_response) | ||||
|  |     for i in range(len(artist_objs)): | ||||
|  |         if len(artists_response[i]['genres']) == 0: | ||||
|  |             process_artist_genre("undefined", artist_objs[i]) | ||||
|  |         else: | ||||
|  |             for genre in artists_response[i]['genres']: | ||||
|  |                 process_artist_genre(genre, artist_objs[i]) | ||||
| 
 | 
 | ||||
| #  }}} calculate_genres_from_artists #  |  | ||||
|  | #  }}}  add_artist_genres #  | ||||
| 
 | 
 | ||||
| def process_library_stats(library_stats): |  | ||||
|     """Processes library_stats into format more suitable for D3 consumption |  | ||||
|  | #  get_artists_in_genre {{{ #  | ||||
| 
 | 
 | ||||
|     Args: |  | ||||
|         library_stats: Dictionary containing the data mined from user's Spotify library |  | ||||
|  | def get_artists_in_genre(user, genre, max_songs): | ||||
|  |     """Return count of artists in genre. | ||||
| 
 | 
 | ||||
|     Returns: |  | ||||
|         A new dictionary that contains the data in library_stats, in a format more suitable for D3 consumption |  | ||||
|  |     :user: User object to return data for. | ||||
|  |     :genre: genre to count artists for. | ||||
|  |     :max_songs: max total songs to include to prevent overflow due to having | ||||
|  |     multiple artists on each track. | ||||
|  | 
 | ||||
|  |     :returns: dict of artists in the genre along with the number of songs they | ||||
|  |     have.  | ||||
|     """ |     """ | ||||
|     processed_library_stats = {} |  | ||||
|     for key in library_stats: |  | ||||
|         if key == 'artists' or key == 'genres' or key == 'year_released': |  | ||||
|             for inner_key in library_stats[key]: |  | ||||
|                 if key not in processed_library_stats: |  | ||||
|                     processed_library_stats[key] = [] |  | ||||
|                 processed_item_key = '' # identifier key for each dict in the list |  | ||||
|                 count = 0 |  | ||||
|                 if 'artist' in key: |  | ||||
|                     processed_item_key = 'name' |  | ||||
|                     count = library_stats[key][inner_key]['count'] |  | ||||
|                 elif 'genre' in key: |  | ||||
|                     processed_item_key = 'genre' |  | ||||
|                     count = library_stats[key][inner_key] |  | ||||
|                 else: |  | ||||
|                     processed_item_key = 'year' |  | ||||
|                     count = library_stats[key][inner_key] |  | ||||
| 
 |  | ||||
|                 processed_library_stats[key].append({ |  | ||||
|                     processed_item_key: inner_key, |  | ||||
|                     "count": count |  | ||||
|                 }) |  | ||||
|         elif key == 'audio_features': |  | ||||
|             for audio_feature in library_stats[key]: |  | ||||
|                 if 'audio_features' not in processed_library_stats: |  | ||||
|                     processed_library_stats['audio_features'] = [] |  | ||||
|                 processed_library_stats['audio_features'].append({ |  | ||||
|                     'feature': audio_feature, |  | ||||
|                     'average': library_stats[key][audio_feature]['average'], |  | ||||
|                     'std_dev': library_stats[key][audio_feature]['std_dev'] |  | ||||
|                 }) |  | ||||
|         # TODO: Not sure about final form for 'popularity' |  | ||||
|         # elif key == 'popularity': |  | ||||
|         #     processed_library_stats[key] = [] |  | ||||
|         #     processed_library_stats[key].append({ |  | ||||
| 
 |  | ||||
|         #     }) |  | ||||
|         elif key == 'num_songs' or key == 'total_runtime' or key == 'popularity': |  | ||||
|             processed_library_stats[key] = library_stats[key] |  | ||||
|      |  | ||||
|     return processed_library_stats |  | ||||
|  |     genre_obj = Genre.objects.get(name=genre) | ||||
|  |     artist_counts = (Artist.objects.filter(track__users=user) | ||||
|  |             .filter(genres=genre_obj)  | ||||
|  |             .annotate(num_songs=Count('track', distinct=True)) | ||||
|  |             .order_by('-num_songs') | ||||
|  |             ) | ||||
|  |     processed_artist_counts = {} | ||||
|  |     songs_added = 0 | ||||
|  |     for artist in artist_counts: | ||||
|  |         # hacky way to not have total count overflow due to there being multiple | ||||
|  |         # artists on a track | ||||
|  |         if songs_added + artist.num_songs <= max_songs: | ||||
|  |             processed_artist_counts[artist.name] = artist.num_songs | ||||
|  |             songs_added += artist.num_songs | ||||
|  |     #  processed_artist_counts = [{'name': artist.name, 'num_songs': artist.num_songs} for artist in artist_counts] | ||||
|  |     #  processed_artist_counts = {artist.name: artist.num_songs for artist in artist_counts} | ||||
|  |     #  pprint.pprint(processed_artist_counts) | ||||
|  |     return processed_artist_counts | ||||
|  | 
 | ||||
|  | #  }}} get_artists_in_genre #  | ||||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue