diff --git a/.gitignore b/.gitignore index 941775f..c492bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ db.sqlite3 *.bak .idea/ .vscode/* +*/migrations/* api-keys.sh Pipfile -super-pass.txt -*.js -*.ini +*.txt +scrap.py diff --git a/musicvis/settings.py b/musicvis/settings.py index 7eefdbb..0cedb25 100644 --- a/musicvis/settings.py +++ b/musicvis/settings.py @@ -110,7 +110,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'America/Toronto' USE_I18N = True diff --git a/recreate-db.txt b/recreate-db.txt new file mode 100644 index 0000000..5c1e574 --- /dev/null +++ b/recreate-db.txt @@ -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 diff --git a/requirements.txt b/requirements.txt index 7eba139..59c9dc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ idna==2.6 isort==4.3.4 lazy-object-proxy==1.3.1 mccabe==0.6.1 -psycopg2==2.7.4 +psycopg2-binary==2.7.4 pylint==1.8.4 pytz==2018.4 requests==2.18.4 diff --git a/sample-track-obj.json b/sample-track-obj.py similarity index 100% rename from sample-track-obj.json rename to sample-track-obj.py diff --git a/spotifyvis/admin.py b/spotifyvis/admin.py index 8c38f3f..bd71265 100644 --- a/spotifyvis/admin.py +++ b/spotifyvis/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin +from .models import Track, Artist, AudioFeatures, User # Register your models here. +admin.site.register(Track) +admin.site.register(Artist) +admin.site.register(AudioFeatures) +admin.site.register(User) diff --git a/spotifyvis/migrations/0001_initial.py b/spotifyvis/migrations/0001_initial.py deleted file mode 100644 index e22ffd2..0000000 --- a/spotifyvis/migrations/0001_initial.py +++ /dev/null @@ -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')}, - ), - ] diff --git a/spotifyvis/models.py b/spotifyvis/models.py index 773492c..7419443 100644 --- a/spotifyvis/models.py +++ b/spotifyvis/models.py @@ -1,50 +1,86 @@ from django.db import models +# id's are 22 in length in examples but set to 30 for buffer +MAX_ID = 30 + +# Genre {{{ # + +class Genre(models.Model): + + class Meta: + verbose_name = "Genre" + verbose_name_plural = "Genres" + + name = models.CharField(primary_key=True, max_length=50) + num_songs = models.PositiveIntegerField() + + def __str__(self): + return self.name + +# }}} Genre # + +# Artist {{{ # + class Artist(models.Model): class Meta: verbose_name = "Artist" verbose_name_plural = "Artists" - artist_id = models.CharField(primary_key=True, max_length=30) + artist_id = models.CharField(primary_key=True, max_length=MAX_ID) # unique since only storing one genre per artist right now name = models.CharField(unique=True, max_length=50) - genre = models.CharField(max_length=20) + genres = models.ManyToManyField(Genre, blank=True) def __str__(self): return self.name +# }}} Artist # + +# User {{{ # class User(models.Model): class Meta: verbose_name = "User" verbose_name_plural = "Users" - user_id = models.CharField(primary_key=True, max_length=30) # the user's Spotify ID - username = models.CharField(max_length=30) # User's Spotify user name, if set + user_id = models.CharField(primary_key=True, max_length=MAX_ID) # the user's Spotify ID + user_secret = models.CharField(max_length=50, default='') def __str__(self): - return self.username + return self.user_id +# }}} User # + +# Track {{{ # class Track(models.Model): class Meta: verbose_name = "Track" verbose_name_plural = "Tracks" - unique_together = ('track_id', 'artist',) - track_id = models.CharField(max_length=30) - artist = models.ForeignKey(Artist, on_delete=models.CASCADE) + track_id = models.CharField(primary_key=True, max_length=MAX_ID) + # artist = models.ForeignKey(Artist, on_delete=models.CASCADE) + artists = models.ManyToManyField(Artist, blank=True) year = models.PositiveSmallIntegerField() - popularity = models.DecimalField(decimal_places=2, max_digits=2) + popularity = models.PositiveSmallIntegerField() runtime = models.PositiveSmallIntegerField() - name = models.CharField(max_length=75) - users = models.ManyToManyField(User) + name = models.CharField(max_length=200) + users = models.ManyToManyField(User, blank=True) + genre = models.ForeignKey(Genre, on_delete=models.CASCADE, blank=True, + null=True) def __str__(self): - return self.name + track_str = "{}, genre: {}, artists: [".format(self.name, self.genre) + for artist in self.artists.all(): + track_str += "{}, ".format(artist.name) + track_str += "]" + return track_str + +# }}} Track # +# AudioFeatures {{{ # class AudioFeatures(models.Model): @@ -53,14 +89,16 @@ class AudioFeatures(models.Model): verbose_name_plural = "AudioFeatures" track = models.OneToOneField(Track, on_delete=models.CASCADE, primary_key=True,) - 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) + acousticness = models.DecimalField(decimal_places=3, max_digits=3) + danceability = models.DecimalField(decimal_places=3, max_digits=3) + energy = models.DecimalField(decimal_places=3, max_digits=3) + instrumentalness = models.DecimalField(decimal_places=3, max_digits=3) + loudness = models.DecimalField(decimal_places=3, max_digits=6) + speechiness = models.DecimalField(decimal_places=3, max_digits=3) + tempo = models.DecimalField(decimal_places=3, max_digits=6) + valence = models.DecimalField(decimal_places=3, max_digits=3) def __str__(self): return super(AudioFeatures, self).__str__() + +# }}} AudioFeatures # diff --git a/spotifyvis/static/spotifyvis/css/dark_bg.css b/spotifyvis/static/spotifyvis/css/dark_bg.css new file mode 100644 index 0000000..a472959 --- /dev/null +++ b/spotifyvis/static/spotifyvis/css/dark_bg.css @@ -0,0 +1,8 @@ +body { +background-color: #1e1e1e; +} + +h1,p { +color: grey; +} + diff --git a/spotifyvis/static/spotifyvis/scripts/genre_graph.js b/spotifyvis/static/spotifyvis/scripts/genre_graph.js new file mode 100644 index 0000000..6422d22 --- /dev/null +++ b/spotifyvis/static/spotifyvis/scripts/genre_graph.js @@ -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 // diff --git a/spotifyvis/static/spotifyvis/scripts/user_data.js b/spotifyvis/static/spotifyvis/scripts/user_data.js deleted file mode 100644 index e69de29..0000000 diff --git a/spotifyvis/templates/spotifyvis/audio_features.html b/spotifyvis/templates/spotifyvis/audio_features.html new file mode 100644 index 0000000..47db0fd --- /dev/null +++ b/spotifyvis/templates/spotifyvis/audio_features.html @@ -0,0 +1,141 @@ +{% load static %} + + + + + +
+ + +Logged in as {{ user_id }}
+ + + + diff --git a/spotifyvis/templates/spotifyvis/genre_graph.html b/spotifyvis/templates/spotifyvis/genre_graph.html new file mode 100644 index 0000000..a33edae --- /dev/null +++ b/spotifyvis/templates/spotifyvis/genre_graph.html @@ -0,0 +1,44 @@ + + + + + + +{% load static %} + + + + +