minimum viable UI. CAA-related scripts

This commit is contained in:
simon987 2019-05-08 20:36:42 -04:00
parent 85255c5dc1
commit b588c0eb11
24 changed files with 1256 additions and 338 deletions

View File

@ -5,3 +5,46 @@
wip
### Data import from MusicBrainz & Last.fm
```bash
# Download latest database dump
./get_musicbrainz_dump.sh
# Convert to .csv
python convert_mb.py
# Generate scraping tasks for task_tracker_drone (See notes)
python generate_scrape_tasks.py
# Apply last.fm data to artist.csv
python patch_artists_with_lastfm.py "/path/to/lasfm_data.db"
# Expose generated .csv data to the network
cd repo/
python -m http.server 9999
# On the machine where neo4j is installed:
./import.sh
```
### task_tracker setup:
Last.fm api calls are queued to [task_tracker](https://github.com/simon987/task_tracker/),
and results are gathered by a [task_tracker_drone](https://github.com/simon987/task_tracker_drone/)
([script](https://git.simon987.net/drone/last.fm/src/master/run)).
Project secret:
```json
{
"apikey": "<Your Last.fm api key>",
"user": "<Your Last.fm username>"
}
```
### Api setup
See [music-graph-api](https://github.com/simon987/music-graph-api) for setup instructions.

View File

@ -11,8 +11,9 @@ wget -nc "http://ftp.musicbrainz.org/pub/musicbrainz/data/fullexport/${latest}/m
tar -xjvf mbdump.tar.bz2 mbdump/area mbdump/artist mbdump/l_area_area mbdump/l_artist_artist \
mbdump/l_artist_release mbdump/l_artist_release_group mbdump/l_label_label mbdump/l_release_group_release_group \
mbdump/label mbdump/label_type mbdump/link mbdump/link_type mbdump/release mbdump/release_group \
mbdump/release_group_primary_type
tar -xjvf mbdump-derived.tar.bz2 mbdump/artist_tag mbdump/release_group_tag mbdump/tag mbdump/tag_relation
mbdump/release_group_primary_type mbdump/artist_credit_name mbdump/release_status
tar -xjvf mbdump-derived.tar.bz2 mbdump/artist_tag mbdump/release_group_tag mbdump/tag mbdump/tag_relation \
mbdump/release_group_meta
mv mbdump/* .
rm -r mbdump

27
extract_covers.py Normal file
View File

@ -0,0 +1,27 @@
import sqlite3
import sys
with sqlite3.connect(sys.argv[1]) as conn:
cursor = conn.cursor()
cursor.execute("SELECT id from covers")
cursor = conn.cursor()
cursor.execute("SELECT id from covers")
def rows():
buf = list()
for row in cursor.fetchall():
buf.append(row[0])
if len(buf) > 30:
yield buf
buf.clear()
for batch in rows():
cursor.execute("SELECT cover from covers where id in (%s)" % (",".join(("'" + b + "'") for b in batch)))
covers = cursor.fetchall()
for i, cover in enumerate(covers):
with open("./tmpcovers/" + batch[i] + ".jpg", "wb") as out:
out.write(cover[0])
print(batch[i])

56
generate_caa_tasks.py Normal file
View File

@ -0,0 +1,56 @@
import json
from multiprocessing.pool import ThreadPool
from task_tracker_drone.src.tt_drone.api import TaskTrackerApi, Worker
TT_API_URL = "https://tt.simon987.net/api"
TT_PROJECT = 5
done = set()
# with sqlite3.connect(sys.argv[1]) as conn:
# cur = conn.cursor()
# cur.execute("SELECT id FROM covers")
# for mbid in cur.fetchall():
# done.add(mbid[0])
api = TaskTrackerApi(TT_API_URL)
worker = Worker.from_file(api)
if not worker:
worker = api.make_worker("caa scraper")
worker.dump_to_file()
worker.request_access(TT_PROJECT, True, True)
input("Give permission to " + worker.alias)
def mktask(mbids):
res = worker.submit_task(
project=TT_PROJECT,
recipe=json.dumps(mbids),
hash64=hash(mbids[0]),
max_assign_time=60 * 30,
priority=1,
unique_str=None,
verification_count=None,
max_retries=5,
)
print(res.text)
def lines():
with open("in/release") as f:
buf = list()
for line in f:
cols = line.split("\t")
buf.append(cols[1])
if len(buf) == 75:
a = list(buf)
buf.clear()
yield a
pool = ThreadPool(processes=20)
pool.map(func=mktask, iterable=lines())

48
generate_lastfm_tasks.py Normal file
View File

@ -0,0 +1,48 @@
import csv
import json
from multiprocessing.pool import ThreadPool
from task_tracker_drone.src.tt_drone.api import TaskTrackerApi, Worker
TT_API_URL = "https://tt.simon987.net/api"
TT_PROJECT = 1
api = TaskTrackerApi(TT_API_URL)
worker = Worker.from_file(api)
if not worker:
worker = api.make_worker("last.fm scraper")
worker.dump_to_file()
worker.request_access(TT_PROJECT, True, True)
input("Give permission to " + worker.alias)
with open("repo/artist.csv") as f:
reader = csv.reader(f)
def mktask(lines):
res = worker.submit_task(
project=TT_PROJECT,
recipe=json.dumps(
[{"mbid": line[0], "name": line[1]} for line in lines]
),
unique_str=lines[0][0],
max_assign_time=60 * 5,
)
print(res.text)
def lines():
line_batch = list()
for line in reader:
if "Group" in line[3]:
line_batch.append(line)
if len(line_batch) >= 30:
res = list(line_batch)
line_batch.clear()
yield res
tasks = list(lines())
pool = ThreadPool(processes=25)
pool.map(func=mktask, iterable=tasks)

View File

@ -1,39 +0,0 @@
import csv
import json
from multiprocessing.pool import ThreadPool
from task_tracker_drone.src.tt_drone.api import TaskTrackerApi, Worker
TT_API_URL = "https://tt.simon987.net/api"
TT_PROJECT = 1
api = TaskTrackerApi(TT_API_URL)
worker = Worker.from_file(api)
if not worker:
worker = api.make_worker("last.fm scraper")
worker.dump_to_file()
worker.request_access(TT_PROJECT, True, True)
input("Give permission to " + worker.alias)
with open("repo/artist.csv") as f:
reader = csv.reader(f)
def mktask(line):
res = worker.submit_task(
project=TT_PROJECT,
recipe=json.dumps({"mbid": line[0], "name": line[1]}),
unique_str=line[0],
max_assign_time=60 * 5,
)
print(res.text)
def lines():
for line in reader:
if "Group" in line[2]:
yield line
pool = ThreadPool(processes=60)
pool.map(func=mktask, iterable=lines())

View File

@ -1 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env bash
git submodule init
git submodule update --remote

@ -1 +1 @@
Subproject commit da0874207e2e214ee72be818233e2bf2d30ded19
Subproject commit 855df64c316930062ff4f7740492d0f039788498

View File

@ -6,8 +6,12 @@ export DATABASE="graph.db"
rm -rf "${NEO4J_HOME}/data/databases/${DATABASE}"
cp ${NEO4J_HOME}/conf/neo4j.conf ${NEO4J_HOME}/conf/neo4j.conf.bak
echo "dbms.security.auth_enabled=false" >> ${NEO4J_HOME}/conf/neo4j.conf
mkdir workspace 2> /dev/null
cd workspace
rm *.csv
wget ${REPOSITORY}/area.csv
wget ${REPOSITORY}/area_area.csv

31
make_release_to_rg_map.py Normal file
View File

@ -0,0 +1,31 @@
import sqlite3
release_to_release_group_map = dict()
release_groups = dict()
with open("in/release_group") as f:
for line in f:
cols = line.split("\t")
release_groups[cols[0]] = cols[1]
with open("in/release") as f:
for line in f:
cols = line.split("\t")
release_to_release_group_map[cols[1]] = release_groups[cols[4]]
with sqlite3.connect("mapdb.db") as conn:
cursor = conn.cursor()
cursor.execute("CREATE TABLE map (release TEXT PRIMARY KEY , release_group TEXT)")
for k, v in release_to_release_group_map.items():
cursor.execute("INSERT INTO map (release, release_group) VALUES (?,?)", (k, v))
conn.commit()
"""
CREATE TABLE covers (id TEXT primary key, cover BLOB);
ATTACH 'mapdb.db' AS map;
ATTACH '/mnt/Data8/caa_tn_only.db' AS source;
INSERT OR IGNORE INTO covers SELECT release_group, cover FROM source.covers INNER JOIN map.map ON id = map.release;
"""

View File

@ -1,74 +0,0 @@
import csv
import json
import sqlite3
import sys
def patch(lastfm_data):
with sqlite3.connect(lastfm_data) as conn:
cur = conn.cursor()
cur.execute("SELECT data FROM lastfmdata", )
data = cur.fetchall()
if data:
buffer = []
dup_buf = set()
artist_listeners = dict()
artists = set()
for row in data:
lastfm_data = json.loads(row[0])
for similar in [s for s in lastfm_data["similar"] if s["mbid"] is not None]:
if (similar["mbid"], lastfm_data["artist"]) not in dup_buf:
buffer.append((
similar["mbid"],
lastfm_data["artist"],
similar["match"]
))
dup_buf.add((similar["mbid"], lastfm_data["artist"]))
dup_buf.add((lastfm_data["artist"], similar["mbid"]))
artist_listeners[lastfm_data["artist"]] = (lastfm_data["listeners"], lastfm_data["playcount"])
del dup_buf
with open("repo/lastfm_artist.csv", "w") as out:
writer = csv.writer(out)
writer.writerow([
"id:ID(Artist)", "name", ":LABEL", "listeners:int", "playcount:int"
])
with open("repo/artist.csv") as f:
reader = csv.reader(f)
reader.__next__() # Skip header
for row in reader:
writer.writerow([
row[0],
row[1],
row[2],
row[3],
artist_listeners.get(row[0], (0, 0))[0],
artist_listeners.get(row[0], (0, 0))[1],
])
artists.add(row[0])
with open("repo/lastfm_artist_artist.csv", "w") as out:
out.write(",".join((
":START_ID(Artist)", ":END_ID(Artist)", "weight"
)) + "\n")
for x in buffer:
if x[0] not in artists:
continue
if x[1] not in artists:
continue
out.write(",".join(x) + "\n")
patch(sys.argv[1])

100
process_lastfm_data.py Normal file
View File

@ -0,0 +1,100 @@
import csv
import json
import sqlite3
from collections import defaultdict
import sys
artists = set()
def disambiguate(lfm_artist, artist_release_count, name, mbid):
existing_mbid = lfm_artist.get(name, None)
if existing_mbid and mbid != existing_mbid:
if artist_release_count[existing_mbid] < artist_release_count[mbid]:
lfm_artist[name] = mbid
print("Replacing %s (%s) with %s (%d) for %s" %
(existing_mbid, artist_release_count[existing_mbid],
mbid, artist_release_count[mbid],
name))
else:
lfm_artist[name] = mbid
def patch(lastfm_data):
artist_listeners = dict()
lastfm_artist_to_mbid = dict()
artist_release_count = defaultdict(int)
related = list()
with open("repo/artist_release.csv") as f:
for line in f:
cols = line.split(',')
artist_release_count[cols[0]] += 1
with sqlite3.connect(lastfm_data) as conn:
cur = conn.cursor()
cur.execute("SELECT data FROM lastfmdata", )
data = list(cur.fetchall())
# A lastfm artist name can refer to multiple MBIDs
# For RELATED_TO purposes, we assume that the MBID referring
# to the artist with the most official releases is the one
for row in data:
meta = json.loads(row[0])
disambiguate(lastfm_artist_to_mbid, artist_release_count, meta["name"], meta["artist"])
for similar in [s for s in meta["similar"] if s["mbid"] is not None]:
disambiguate(lastfm_artist_to_mbid, artist_release_count, similar["name"], similar["mbid"])
# Get related links & listener counts
for row in data:
meta = json.loads(row[0])
artist_listeners[lastfm_artist_to_mbid[meta["name"]]] = \
(meta["listeners"], meta["playcount"])
for similar in [s for s in meta["similar"] if s["mbid"] is not None]:
related.append((
lastfm_artist_to_mbid[similar["name"]],
lastfm_artist_to_mbid[meta["name"]],
similar["match"]
))
with open("repo/lastfm_artist.csv", "w") as out:
writer = csv.writer(out)
writer.writerow([
"id:ID(Artist)", "name", "year:short", ":LABEL", "listeners:int", "playcount:int"
])
with open("repo/artist.csv") as f:
reader = csv.reader(f)
reader.__next__() # Skip header
for row in reader:
writer.writerow([
row[0],
row[1],
row[2],
row[3],
artist_listeners.get(row[0], (0, 0))[0],
artist_listeners.get(row[0], (0, 0))[1],
])
artists.add(row[0])
with open("repo/lastfm_artist_artist.csv", "w") as out:
out.write(",".join((
":START_ID(Artist)", ":END_ID(Artist)", "weight:float"
)) + "\n")
for x in related:
if x[0] in artists and x[1] in artists:
out.write(",".join(x) + "\n")
patch(sys.argv[1])

View File

@ -1,4 +1,5 @@
import os
from collections import defaultdict
links = dict()
link_types = dict()
@ -8,6 +9,7 @@ label_types = {
"\\N": ""
}
release_groups = dict()
release_statuses = dict()
release_to_release_group_map = dict()
release_types = {
"\\N": "",
@ -107,6 +109,7 @@ artist_artist_rel_map = {
"subgroup": "IS_SUBGROUP_OF",
"founder": "IS_FOUNDER_OF",
"involved with": "IS_INVOLVED_WITH",
"named after": "IS_NAMED_AFTER",
}
label_label_rel_map = {
@ -132,6 +135,11 @@ with open("in/link", "r") as f:
cols = line.split("\t")
links[cols[0]] = cols
with open("in/release_status", "r") as f:
for line in f:
cols = line.split("\t")
release_statuses[cols[0]] = cols
with open("in/link_type", "r") as f:
for line in f:
cols = line.split("\t")
@ -187,7 +195,7 @@ for _, artist in artists.items():
out_artist.write(",".join((
artist[1],
'"' + artist[2].replace("\"", "\"\"") + '"',
artist[4] if artist[4] != "\\N" else "",
artist[4] if artist[4] != "\\N" else "0",
"Artist" + (";Group\n" if artist[10] == "2" else "\n")
)))
@ -216,8 +224,14 @@ with open("in/release_group_primary_type") as f:
cols = line.split("\t")
release_types[cols[0]] = ";" + cols[1]
release_group_year = dict()
with open("in/release_group_meta") as f:
for line in f:
cols = line.split("\t")
release_group_year[cols[0]] = cols[2] if cols[2] != "\\N" else "0"
with open("repo/release.csv", "w") as out:
out.write("id:ID(Release),name,:LABEL\n")
out.write("id:ID(Release),name,year:int,:LABEL\n")
with open("in/release_group") as f:
for line in f:
@ -225,6 +239,7 @@ with open("repo/release.csv", "w") as out:
out.write(",".join((
cols[1],
'"' + cols[2].replace("\"", "\"\"") + '"',
release_group_year[cols[0]],
"Release" + release_types[cols[4]],
)) + "\n")
@ -233,19 +248,46 @@ with open("repo/release.csv", "w") as out:
with open("in/release") as f:
for line in f:
cols = line.split("\t")
release_to_release_group_map[cols[0]] = cols[4]
if cols[5] != '\\N' and release_statuses[cols[5]][1] == "Official":
release_to_release_group_map[cols[0]] = cols[4]
credit_names = defaultdict(list)
with open("in/artist_credit_name") as f:
for line in f:
cols = line.split("\t")
credit_names[cols[0]].append(artists[cols[2]][1])
with open("tmp/tmp_artist_release.csv", "w") as out:
out.write(":START_ID(Artist),:END_ID(Release),:TYPE\n")
# Is this part really necessary?
with open("in/l_artist_release") as f:
for line in f:
cols = line.split("\t")
out.write(",".join((
artists[cols[2]][1],
release_groups[release_to_release_group_map[cols[3]]][1],
artist_release_rel_map[link_types[links[cols[1]][1]][6]]
)) + "\n")
if cols[3] in release_to_release_group_map:
out.write(",".join((
artists[cols[2]][1],
release_groups[release_to_release_group_map[cols[3]]][1],
artist_release_rel_map[link_types[links[cols[1]][1]][6]]
)) + "\n")
# Artist credits
with open("in/release") as f:
for line in f:
cols = line.split("\t")
if cols[0] in release_to_release_group_map:
for credit in credit_names[cols[3]]:
out.write(",".join((
credit,
release_groups[release_to_release_group_map[cols[0]]][1],
"CREDITED_FOR"
)) + "\n")
# Remove dupes
os.system("(head -n 1 tmp/tmp_artist_release.csv && tail -n +2 tmp/tmp_artist_release.csv"
" | sort) | uniq > repo/artist_release.csv && rm tmp/tmp_artist_release.csv")
with open("repo/release_release.csv", "w") as out:
out.write(":START_ID(Release),:END_ID(Release),:TYPE\n")
@ -259,12 +301,8 @@ with open("repo/release_release.csv", "w") as out:
release_release_rel_map[link_types[links[cols[1]][1]][6]]
)) + "\n")
os.system("(head -n 1 tmp/tmp_artist_release.csv && tail -n +2 tmp/tmp_artist_release.csv"
" | sort) | uniq > repo/artist_release.csv && rm tmp/tmp_artist_release.csv")
# ---
with open("in/tag") as f:
with open("repo/tag.csv", "w") as out:
out.write("id:ID(Tag),name\n")
@ -307,7 +345,7 @@ with open("repo/artist_tag.csv", "w") as out:
)) + "\n")
with open("repo/tag_tag.csv", "w") as out:
out.write(":START_ID(Tag),:END_ID(Tag),weight\n")
out.write(":START_ID(Tag),:END_ID(Tag),weight:int\n")
with open("in/tag_relation") as f:
for line in f:

2
seed.cypher Normal file
View File

@ -0,0 +1,2 @@
CREATE INDEX ON :Artist(id);
CREATE INDEX ON :Release(id);

5
seed_neo4j_db.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
export NEO4J_HOME="/home/drone/Downloads/neo4j-community-3.5.3"
cat seed.cypher | ${NEO4J_HOME}/bin/cypher-shell

@ -1 +1 @@
Subproject commit 69af3ba908c3c3e1c838da3e95ff15f967086af0
Subproject commit e025596cf2ccfbe803f05cb848927ae36fe270a3

View File

@ -25,10 +25,10 @@ module.exports = {
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
"indent": "off",
"indent-legacy": ["error", 4],
"indent": ["error", 4, {SwitchCase: 1}],
"no-param-reassign": 0,
"no-underscore-dangle": 0,
"no-trailing-spaces": 0,
"space-before-function-paren": 0,
}
};

363
ui/package-lock.json generated
View File

@ -60,6 +60,35 @@
"@babel/types": "7.0.0-beta.44"
}
},
"@babel/helper-module-imports": {
"version": "7.0.0-beta.35",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.35.tgz",
"integrity": "sha512-vaC1KyIZSuyWb3Lj277fX0pxivyHwuDU4xZsofqgYAbkDxNieMg2vuhzP5AgMweMY7fCQUMTi+BgPqTLjkxXFg==",
"dev": true,
"requires": {
"@babel/types": "7.0.0-beta.35",
"lodash": "^4.2.0"
},
"dependencies": {
"@babel/types": {
"version": "7.0.0-beta.35",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.35.tgz",
"integrity": "sha512-y9XT11CozHDgjWcTdxmhSj13rJVXpa5ZXwjjOiTedjaM0ba5ItqdS02t31EhPl7HtOWxsZkYCCUNrSfrOisA6w==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.2.0",
"to-fast-properties": "^2.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
}
}
},
"@babel/helper-split-export-declaration": {
"version": "7.0.0-beta.44",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz",
@ -166,6 +195,11 @@
}
}
},
"@sindresorhus/is": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow=="
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
@ -439,6 +473,14 @@
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
"dev": true
},
"async-validator": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-1.8.5.tgz",
"integrity": "sha512-tXBM+1m056MAX0E8TL2iCjg8WvSyXu0Zc8LNtYqrVeyoL3+esHRZ4SieE9fKQyyU09uONjnMEjrNBMqT0mbvmA==",
"requires": {
"babel-runtime": "6.x"
}
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@ -731,8 +773,7 @@
"babel-helper-vue-jsx-merge-props": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz",
"integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==",
"dev": true
"integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg=="
},
"babel-helpers": {
"version": "6.24.1",
@ -773,6 +814,15 @@
"babel-runtime": "^6.22.0"
}
},
"babel-plugin-component": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/babel-plugin-component/-/babel-plugin-component-1.1.1.tgz",
"integrity": "sha512-WUw887kJf2GH80Ng/ZMctKZ511iamHNqPhd9uKo14yzisvV7Wt1EckIrb8oq/uCz3B3PpAW7Xfl7AkTLDYT6ag==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "7.0.0-beta.35"
}
},
"babel-plugin-syntax-async-functions": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
@ -1260,7 +1310,6 @@
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
"dev": true,
"requires": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
@ -1690,6 +1739,60 @@
"unset-value": "^1.0.0"
}
},
"cacheable-request": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz",
"integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=",
"requires": {
"clone-response": "1.0.2",
"get-stream": "3.0.0",
"http-cache-semantics": "3.8.1",
"keyv": "3.0.0",
"lowercase-keys": "1.0.0",
"normalize-url": "2.0.1",
"responselike": "1.0.2"
},
"dependencies": {
"lowercase-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz",
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY="
},
"normalize-url": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz",
"integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==",
"requires": {
"prepend-http": "^2.0.0",
"query-string": "^5.0.1",
"sort-keys": "^2.0.0"
}
},
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
},
"query-string": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
"requires": {
"decode-uri-component": "^0.2.0",
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
}
},
"sort-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz",
"integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=",
"requires": {
"is-plain-obj": "^1.0.0"
}
}
}
},
"caller-path": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
@ -1964,6 +2067,14 @@
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=",
"dev": true
},
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
"requires": {
"mimic-response": "^1.0.0"
}
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -2220,14 +2331,12 @@
"core-js": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
"integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==",
"dev": true
"integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "4.0.0",
@ -2954,8 +3063,15 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"requires": {
"mimic-response": "^1.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
@ -2969,6 +3085,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
"deepmerge": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz",
"integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ=="
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -3215,6 +3336,11 @@
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
"dev": true
},
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -3245,6 +3371,19 @@
"integrity": "sha512-glecGr/kFdfeXUHOHAWvGcXrxNU+1wSO/t5B23tT1dtlvYB26GY8aHzZSWD7HqhqC800Lr+w/hQul6C5AF542w==",
"dev": true
},
"element-ui": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/element-ui/-/element-ui-2.7.2.tgz",
"integrity": "sha512-Exh9QTkm9gwMMPzg1TyaTlBKyr3k4K9XcC5vl0A/mneDvJX//RsURGuOWsCNDVQMdhh5h9e+W5icosh+pKfbCg==",
"requires": {
"async-validator": "~1.8.1",
"babel-helper-vue-jsx-merge-props": "^2.0.0",
"deepmerge": "^1.2.0",
"normalize-wheel": "^1.0.1",
"resize-observer-polyfill": "^1.5.0",
"throttle-debounce": "^1.0.1"
}
},
"elliptic": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
@ -4311,7 +4450,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
"integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"readable-stream": "^2.0.0"
@ -4910,8 +5048,7 @@
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"get-value": {
"version": "2.0.6",
@ -4974,6 +5111,30 @@
"slash": "^1.0.0"
}
},
"got": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz",
"integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==",
"requires": {
"@sindresorhus/is": "^0.7.0",
"cacheable-request": "^2.1.1",
"decompress-response": "^3.3.0",
"duplexer3": "^0.1.4",
"get-stream": "^3.0.0",
"into-stream": "^3.1.0",
"is-retry-allowed": "^1.1.0",
"isurl": "^1.0.0-alpha5",
"lowercase-keys": "^1.0.0",
"mimic-response": "^1.0.0",
"p-cancelable": "^0.4.0",
"p-timeout": "^2.0.1",
"pify": "^3.0.0",
"safe-buffer": "^5.1.1",
"timed-out": "^4.0.1",
"url-parse-lax": "^3.0.0",
"url-to-options": "^1.0.1"
}
},
"graceful-fs": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
@ -5026,12 +5187,25 @@
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"has-symbol-support-x": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz",
"integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw=="
},
"has-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
"dev": true
},
"has-to-string-tag-x": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz",
"integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==",
"requires": {
"has-symbol-support-x": "^1.4.1"
}
},
"has-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@ -5223,6 +5397,11 @@
}
}
},
"http-cache-semantics": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz",
"integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w=="
},
"http-deceiver": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
@ -5393,8 +5572,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"inquirer": {
"version": "3.3.0",
@ -5450,6 +5628,15 @@
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
"dev": true
},
"into-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz",
"integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=",
"requires": {
"from2": "^2.1.1",
"p-is-promise": "^1.1.0"
}
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -5604,6 +5791,11 @@
"kind-of": "^3.0.2"
}
},
"is-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz",
"integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA="
},
"is-path-cwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
@ -5631,8 +5823,7 @@
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
"dev": true
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-plain-object": {
"version": "2.0.4",
@ -5664,6 +5855,11 @@
"integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==",
"dev": true
},
"is-retry-allowed": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
@ -5709,8 +5905,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
@ -5724,6 +5919,15 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"isurl": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
"integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
"requires": {
"has-to-string-tag-x": "^1.2.0",
"is-object": "^1.0.1"
}
},
"js-base64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
@ -5752,6 +5956,11 @@
"integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
"dev": true
},
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
},
"json-loader": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
@ -5788,6 +5997,14 @@
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
},
"keyv": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz",
"integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==",
"requires": {
"json-buffer": "3.0.0"
}
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -6023,6 +6240,11 @@
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
"dev": true
},
"lowercase-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
},
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
@ -6288,6 +6510,11 @@
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
"dev": true
},
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -6566,6 +6793,11 @@
"sort-keys": "^1.0.0"
}
},
"normalize-wheel": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
"integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
},
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@ -6599,8 +6831,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@ -6782,11 +7013,20 @@
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
"p-cancelable": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz",
"integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ=="
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-is-promise": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz",
"integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4="
},
"p-limit": {
"version": "1.3.0",
@ -6812,6 +7052,14 @@
"integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==",
"dev": true
},
"p-timeout": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz",
"integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==",
"requires": {
"p-finally": "^1.0.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@ -6952,8 +7200,7 @@
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pinkie": {
"version": "2.0.4",
@ -9113,8 +9360,7 @@
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"dev": true
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"progress": {
"version": "2.0.3",
@ -9338,7 +9584,6 @@
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@ -9424,8 +9669,7 @@
"regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
"dev": true
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
"regenerator-transform": {
"version": "0.10.1",
@ -9568,6 +9812,11 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
@ -9606,6 +9855,14 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"responselike": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
"integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
"requires": {
"lowercase-keys": "^1.0.0"
}
},
"restore-cursor": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
@ -9691,8 +9948,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-regex": {
"version": "1.1.0",
@ -10313,8 +10569,7 @@
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"dev": true
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-width": {
"version": "2.1.1",
@ -10347,7 +10602,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@ -10458,6 +10712,11 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"throttle-debounce": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-1.1.0.tgz",
"integrity": "sha512-XH8UiPCQcWNuk2LYePibW/4qL97+ZQ1AN3FNXwZRBNPPowo/NRU5fAlDCSNBJIYCKbioZfuYtMhG4quqoJhVzg=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@ -10486,6 +10745,11 @@
"integrity": "sha512-zxke8goJQpBeEgD82CXABeMh0LSJcj7CXEd0OHOg45HgcofF7pxNwZm9+RknpxpDhwN4gFpySkApKfFYfRQnUA==",
"dev": true
},
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
},
"timers-browserify": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz",
@ -10885,6 +11149,26 @@
"requires-port": "^1.0.0"
}
},
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
"requires": {
"prepend-http": "^2.0.0"
},
"dependencies": {
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
}
}
},
"url-to-options": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
"integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k="
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@ -10903,8 +11187,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utila": {
"version": "0.4.0",
@ -11059,6 +11342,14 @@
}
}
},
"vue-resource": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/vue-resource/-/vue-resource-1.5.1.tgz",
"integrity": "sha512-o6V4wNgeqP+9v9b2bPXrr20CGNQPEXjpbUWdZWq9GJhqVeAGcYoeTtn/D4q059ZiyN0DIrDv/ADrQUmlUQcsmg==",
"requires": {
"got": "^8.0.3"
}
},
"vue-router": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.0.2.tgz",

View File

@ -13,7 +13,10 @@
"dependencies": {
"d3": "^5.9.2",
"d3-force": "^2.0.1",
"d3-path": "^1.0.7",
"element-ui": "^2.7.2",
"vue": "^2.6.10",
"vue-resource": "^1.5.1",
"vue-router": "^3.0.2"
},
"devDependencies": {
@ -22,6 +25,7 @@
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-component": "^1.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",

View File

@ -5,18 +5,17 @@
</template>
<script>
export default {
name: 'App'
}
export default {
name: 'App'
}
</script>
<style>
body {
padding: 0;
margin: 0;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<el-card class="artist-info box-card" v-if="artist !== undefined">
<div slot="header">
<span>{{artist.name}}</span>
</div>
<div>
<ImageCarousel
v-bind:sources="artistInfo.covers"
interval="750" />
</div>
</el-card>
</template>
<script>
import Vue from 'vue'
import ImageCarousel from './ImageCarousel'
let data = {
artistInfo: {
releases: [],
covers: []
}
}
function reloadInfo(artist) {
Vue.http.get('http://localhost:3030/artist/details/' + artist.mbid)
.then(response => {
response.json().then(info => {
info.covers = info.releases.map(mbid => 'http://localhost:3030/cover/' + mbid)
data.artistInfo = info
})
})
}
export default {
name: 'ArtistInfo',
components: {ImageCarousel},
props: ['artist'],
watch: {
artist: reloadInfo
},
data() {
return data
}
}
</script>
<style scoped>
.artist-info {
margin: 1rem;
position: fixed;
}
</style>

View File

@ -1,12 +1,19 @@
<template>
<div></div>
<div>
<ArtistInfo v-bind:artist="hoverArtist"/>
</div>
</template>
<script>
import * as d3 from 'd3'
import ArtistInfo from './ArtistInfo'
import * as d3 from 'd3'
let data = {
hoverArtist: undefined
}
function getNodeType (labels) {
const nodeUtils = {
getNodeType: function (labels) {
if (labels.find(l => l === 'Tag')) {
return 'Tag'
} else if (labels.find(l => l === 'Group')) {
@ -15,182 +22,442 @@
return 'Artist'
}
return undefined
},
getRadius: function (node) {
if (expandedNodes.has(node.id)) {
return 40
}
if (node.type === 'Tag') {
return 10
}
return Math.max(Math.sqrt(node.listeners / 7000) * 1.4, 15)
},
getColor: function (node) {
if (expandedNodes.has(node.id)) {
return '#1cb3c8'
}
return null
}
}
let originArtist
let originNode
let expandedNodes = new Set()
function MusicGraph() {
const width = window.innerWidth - 7
const height = window.innerHeight - 7
this.nodeById = new Map()
this.nodes = []
this.links = []
this._originSet = false
this.svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
this.zoomed = () => {
this.container.attr('transform', d3.event.transform)
}
let data = {}
d3.json('../static/data.json')
.then((r) => {
data = r
this.svg.append('rect')
.attr('width', width)
.attr('height', height)
.classed('pan-rect', true)
.style('fill', 'none')
.call(d3.zoom()
.scaleExtent([1 / 10, 5])
.on('zoom', this.zoomed))
const links = data.map(row => ({
source: row._fields[1].start.low,
target: row._fields[1].end.low,
weight: row._fields[1].properties.weight.low
}))
const nodes = []
this.container = this.svg.append('g').attr('id', 'container')
function addNode (node) {
if (nodes.find(n => n.id === node.id)) {
return
}
nodes.push(node)
}
this.container.append('g')
.attr('id', 'links')
this.container.append('g')
.attr('id', 'nodes')
this.container.append('g')
.attr('id', 'labels')
data.forEach((row) => {
addNode({
id: row._fields[0].identity.low,
name: row._fields[0].properties.name,
listeners: row._fields[0].properties.listeners.low,
type: getNodeType(row._fields[0].labels)
})
addNode({
id: row._fields[2].identity.low,
name: row._fields[2].properties.name,
type: 'Tag'
})
})
this.dragStarted = (d) => {
if (!d3.event.active) {
this.simulation.alphaTarget(0.3).restart()
}
d.fx = d.x
d.fy = d.y
}
function getRadius (node) {
if (node.type === 'Tag') {
return 10
}
return Math.max(Math.sqrt(node.listeners / 5000), 15)
}
this.dragged = (d) => {
d.fx = d3.event.x
d.fy = d3.event.y
}
function getColor (node) {
switch (node.type) {
case 'Tag':
return '#e0e0e0'
case 'Artist':
return '#42c3f7'
case 'Group':
return '#00a5e9'
default:
return '#DEADFB'
}
}
this.dragEnded = (d) => {
if (!d3.event.active) {
this.simulation.alphaTarget(0)
}
const width = window.innerWidth - 5
const height = window.innerHeight - 5
d.fx = null
d.fy = null
}
// ??
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id))
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2))
this.nodeHover = (d) => {
data.hoverArtist = d
let container
function zoomed () {
container.attr('transform', d3.event.transform)
}
function nodeZoomed () {
// TODO
}
function dragStarted (d) {
if (!d3.event.active) {
simulation.alphaTarget(0.3).restart()
}
d.fx = d.x
d.fy = d.y
}
function dragged (d) {
d.fx = d3.event.x
d.fy = d3.event.y
}
function dragEnded (d) {
if (!d3.event.active) {
simulation.alphaTarget(0)
}
d.fx = null
d.fy = null
}
const svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
svg.append('rect')
.attr('width', width)
.attr('height', height)
.style('pointer-events', 'all')
.style('fill', 'none')
.call(d3.zoom()
.scaleExtent([1 / 3, 5])
.on('zoom', zoomed))
document.body.setAttribute('style', 'background: #E7EDEB')
container = svg.append('g')
const link = container.append('g')
.attr('stroke', '#003a6b')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke-opacity', rel => rel.weight / 15)
.attr('stroke-width', rel => Math.sqrt(rel.weight) * 0.6)
const node = container.append('g')
.attr('stroke', '#ffffff')
.attr('stroke-width', 1.5)
.selectAll('circle')
.data(nodes)
.join('circle')
.attr('r', d => getRadius(d))
.attr('fill', d => getColor(d))
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded))
.on('wheel', nodeZoomed)
node.append('title')
.text(d => `${d.name} ${d.id}`)
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
node
.attr('cx', d => d.x)
.attr('cy', d => d.y)
})
})
export default {
name: 'hello',
data () {
let srcLinks = this.links.filter(link => link.source.id === d.id)
let targetLinks = this.links.filter(link => link.target.id === d.id)
data.hoverLinks = srcLinks.map(l => {
return {
msg: 'Welcome to Your Vue.js App'
match: (l.weight * 100).toFixed(2) + '%',
other: l.target
}
}).concat(targetLinks.map(l => {
return {
match: (l.weight * 100).toFixed(2) + '%',
other: l.source
}
}))
console.log(data.hoverLinks)
this.svg.classed('hover', true)
this.link.classed('selected', link =>
link.source.id === d.id || link.target.id === d.id)
this.node.classed('selected', n =>
n.sourceLinks.has(d.id) ||
n.targetLinks.has(d.id))
this.label.classed('selected', n =>
n.sourceLinks.has(d.id) ||
n.targetLinks.has(d.id) ||
n.id === d.id)
this.node.classed('hover', n => n.id === d.id)
}
this.nodeOut = () => {
this.svg.classed('hover', false)
this.label.classed('selected', false)
this.link.classed('selected', false)
this.node.classed('selected', false)
this.node.classed('hover', false)
}
this.nodeDbClick = (d) => {
if (expandedNodes.has(d.id)) {
return
}
expandedNodes.add(d.id)
expandArtist(d.mbid)
}
this.simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('collide', d3.forceCollide()
.radius(50)
.strength(1))
.force('center', d3.forceCenter(width / 2, height / 2))
this.simulation.stop()
this.simulation.on('tick', () => {
this.link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
this.node
.attr('cx', d => d.x)
.attr('cy', d => d.y)
this.label
.attr('x', d => d.x)
.attr('y', d => d.y)
})
/**
* Add nodes to the graph
*/
this.addNodes = function (newNodes, relations, originId) {
// Update node map, ignore existing nodes
let nodesToAdd = []
newNodes.forEach(d => {
if (this.nodeById.has(d.id)) {
return
}
this.nodeById.set(d.id, d)
if (this._originSet && originId) {
// Set new nodes initial position
let centerNode = this.nodeById.get(originId)
centerNode.fx = centerNode.x
centerNode.fy = centerNode.y
d.x = centerNode.x
d.y = centerNode.y
setTimeout(() => {
centerNode.fx = null
centerNode.fy = null
}, 600)
}
nodesToAdd.push(d)
})
// Convert {id, id} relation to {node, node}
let linksToAdd = relations.map(({weight, source, target}) => ({
source: this.nodeById.get(source),
target: this.nodeById.get(target),
weight: weight
}))
// Update source/targetLinks
for (const {source, target} of linksToAdd) {
source.sourceLinks.add(target.id)
target.targetLinks.add(source.id)
}
this.nodes.push(...nodesToAdd)
this.links.push(...linksToAdd)
if (!this._originSet) {
this._setOrigin()
this._originSet = true
}
this._update()
}
/**
* Remove nodes from the graph
*/
this.removeNodes = function (idsToRemove) {
let idSetToRemove = new Set(idsToRemove)
idsToRemove.forEach(id => {
// Update targetLinks
Array.from(this.nodeById.get(id).sourceLinks)
.map(srcId => this.nodeById.get(srcId))
.forEach(target => {
target.targetLinks.delete(id)
})
this.nodeById.delete(id)
})
// Remove links
this.links = this.links.filter(l =>
!idSetToRemove.has(l.target.id) &&
!idSetToRemove.has(l.source.id)
)
// Remove nodes
this.nodes = this.nodes.filter(d => !idSetToRemove.has(d.id))
this._update()
}
this._update = function () {
this.simulation.nodes(this.nodes)
this.simulation
.force('link', d3.forceLink(this.links)
.id(d => d.id)
.strength(l => l.weight)
.distance(d => Math.min(
(1.2 / d.weight) * (94 * expandedNodes.size))
)
)
this.simulation
.restart()
// Add new links
this.link = this.container.select('#links')
.selectAll('.link')
.data(this.links)
let linkEnter = this.link
.enter()
.append('line')
.classed('link', true)
this.link = linkEnter.merge(this.link)
// Add new nodes
this.node = this.container.select('#nodes')
.selectAll('.node')
.attr('stroke', d => nodeUtils.getColor(d))
.data(this.nodes)
let nodeEnter = this.node
.enter()
.append('circle')
.classed('node', true)
.attr('r', 35)
.attr('stroke', d => nodeUtils.getColor(d))
.call(d3.drag()
.on('start', this.dragStarted)
.on('drag', this.dragged)
.on('end', this.dragEnded))
.on('mouseover', this.nodeHover)
.on('mouseout', this.nodeOut)
.on('dblclick', this.nodeDbClick)
this.node = nodeEnter.merge(this.node)
// Add new labels
this.label = this.container.select('#labels')
.selectAll('.label')
.data(this.nodes)
let labelEnter = this.label
.enter()
.append('text')
.text(d => d.name)
.classed('label', true)
this.label = labelEnter.merge(this.label)
}
this.setupKeyBindings = function () {
document.body.onkeydown = (e) => {
let isPanMode = this.svg.classed('pan-mode')
if (e.key === 'q') {
this.svg.classed('pan-mode', !isPanMode)
} else if (e.key === 'Escape') {
this.svg.classed('pan-mode', false)
}
}
}
this._setOrigin = function () {
// Set origin node in center
originNode = this.simulation.nodes().find(node => node.id === originArtist.id)
originNode.fx = width / 2
originNode.fy = height / 2
setTimeout(() => {
originNode.fx = null
originNode.fy = null
}, 500)
// Remember that we expanded origin node
expandedNodes.add(originNode.id)
}
this._update()
this.setupKeyBindings()
}
let mm = new MusicGraph()
function expandArtist(mbid) {
d3.json('http://localhost:3030/artist/related/' + mbid)
.then((r) => {
originArtist = r.artists.find(a => a.mbid === mbid)
const nodes = r.artists.map((row) => {
return {
id: row.id,
mbid: row.mbid,
name: row.name,
listeners: row.listeners,
type: nodeUtils.getNodeType(row.labels),
sourceLinks: new Set(),
targetLinks: new Set()
}
})
mm.addNodes(nodes, r.relations, originArtist.id)
})
}
expandArtist('66fc5bf8-daa4-4241-b378-9bc9077939d2')
export default {
components: {ArtistInfo},
data() {
return data
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
<style>
svg {
margin-top: 3px;
margin-left: 3px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
ul {
list-style-type: none;
padding: 0;
/* Pan mode */
svg.pan-mode .node {
pointer-events: none;
}
li {
display: inline-block;
margin: 0 10px;
svg.pan-mode {
cursor: move;
}
a {
color: #42b983;
svg.pan-mode .pan-rect {
pointer-events: all;
}
svg.pan-mode {
box-sizing: border-box;
border: 5px red solid;
}
/* Link */
svg .link.selected {
stroke-width: 2;
stroke-opacity: 1;
}
svg .link {
stroke: orange;
pointer-events: none;
stroke-opacity: 0.7;
stroke-width: 1;
}
svg.hover .link:not(.selected) {
stroke-opacity: 0.2;
stroke-width: 0.1;
}
/* Node */
svg .node.selected {
stroke: red;
}
svg .node.hover {
stroke: red;
}
svg.hover .node:not(.selected):not(.hover) {
display: none;
}
svg .node {
fill: transparent;
}
/* Label */
svg .label {
text-anchor: middle;
pointer-events: none;
}
svg.hover .label:not(.selected) {
display: none;
}
body {
background: #E7EDEB;
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div>
<img
alt=""
width="96"
height="96"
v-show="currentSrc === url"
v-bind:src="url"
v-bind:key="url"
v-for="url in sources"
>
</div>
</template>
<script>
let data = {
currentSrc: '',
index: 0
}
export default {
name: 'ImageCarousel',
props: ['sources', 'interval'],
data() {
return data
},
mounted() {
setInterval(() => {
this.tick()
}, Number(this.interval))
},
watch: {
sources: () => {
data.index = 0
}
},
methods: {
tick() {
if (data.index === this.sources.length - 1) {
data.index = 0
}
data.index += 1
data.currentSrc = this.sources[data.index]
}
}
}
</script>
<style scoped>
</style>

View File

@ -2,7 +2,12 @@ import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
import ElementUI from 'element-ui'
import VueResource from 'vue-resource'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.use(VueResource)
/* eslint-disable no-new */
new Vue({
@ -10,3 +15,5 @@ new Vue({
router,
render: h => h(App)
})
Vue.config.productionTip = false