├── spotify-albums-organizer
├── search_masters.sh
├── run_quicker.sh
├── update_saved_albums.sh
├── remove_saved_albums.sh
├── remove_dbs_collections.sh
├── run.sh
├── remove_all_collections.js
├── remove_masters_data.sh
├── aggregate_masters.js
├── tagify.vim
├── linesfinal.vim
├── show_empty_genre_albums.py
├── import_discogs_masters.sh
├── import_saved_albums.sh
├── import_images.py
├── user_saved_albums.py
├── aggregate_saved_albums.js
├── genres.js
├── get_genres.py
├── discogs_search_update.py
├── clean_data.py
├── display_albums.py
└── xml2json.py
├── display_covers.png
├── LICENSE
├── requirements.txt
└── README.md
/spotify-albums-organizer/search_masters.sh:
--------------------------------------------------------------------------------
1 | mongo < genres.js
--------------------------------------------------------------------------------
/spotify-albums-organizer/run_quicker.sh:
--------------------------------------------------------------------------------
1 | ./import_saved_albums.sh $1
2 | python get_genres.py
--------------------------------------------------------------------------------
/spotify-albums-organizer/update_saved_albums.sh:
--------------------------------------------------------------------------------
1 | ./import_saved_albums.sh $1
2 | ./search_masters.sh
--------------------------------------------------------------------------------
/display_covers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryn3/spotify-albums-organizer/HEAD/display_covers.png
--------------------------------------------------------------------------------
/spotify-albums-organizer/remove_saved_albums.sh:
--------------------------------------------------------------------------------
1 | dir="data/saved_albums"
2 | for f in "$dir"/*; do
3 | rm "$f"
4 | done
--------------------------------------------------------------------------------
/spotify-albums-organizer/remove_dbs_collections.sh:
--------------------------------------------------------------------------------
1 | ./remove_saved_albums.sh
2 | ./remove_masters_data.sh
3 |
4 | mongo < remove_all_collections.js
5 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/run.sh:
--------------------------------------------------------------------------------
1 | ./import_saved_albums.sh $1
2 | ./import_discogs_masters.sh $2
3 | ./search_masters.sh
4 | ./remove_masters_data.sh
5 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/remove_all_collections.js:
--------------------------------------------------------------------------------
1 | db = connect("localhost:27017/discogs_masters");
2 |
3 | db.current_masters.drop()
4 | db.current_albums.drop()
5 | db.temp.drop()
--------------------------------------------------------------------------------
/spotify-albums-organizer/remove_masters_data.sh:
--------------------------------------------------------------------------------
1 | dir="data/masters/XML"
2 | for f in "$dir"/*; do
3 | rm "$f"
4 | done
5 |
6 | dir="data/masters/JSON"
7 | for f in "$dir"/*; do
8 | rm "$f"
9 | done
--------------------------------------------------------------------------------
/spotify-albums-organizer/aggregate_masters.js:
--------------------------------------------------------------------------------
1 | db = connect("localhost:27017/discogs_masters");
2 |
3 | var docs = db.temp.aggregate([{$unwind: {path: "$masters"}}, {$unwind: {path: "$masters.master"}}, {$project: {_id: 0}}])
4 | db.current_masters.insert(docs.toArray())
5 | db.temp.drop()
--------------------------------------------------------------------------------
/spotify-albums-organizer/tagify.vim:
--------------------------------------------------------------------------------
1 | let first = getline(1)
2 | if first !~ ''
3 | execute 'normal! gg'
4 | execute 'put! '
5 | execute 'normal! I'
6 | endif
7 | execute 'normal! G'
8 | let last = line(".")
9 | let last = getline(last)
10 | if last !~ ''
11 | execute 'put '
12 | execute 'normal! I'
13 | endif
14 | execute "wq"
15 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/linesfinal.vim:
--------------------------------------------------------------------------------
1 | function! Lines(query)
2 | let n = line(".")
3 | let m = search(a:query)
4 | let m = line(".")
5 | let nextOccur = m-n
6 | let back = m-n
7 | let k = ''
8 | if nextOccur > 1
9 | while back > 0
10 | let k = k."k"
11 | let back -= 1
12 | endwhile
13 | :execute "normal ".k
14 | :execute "normal $".nextOccur."gJ"
15 | endif
16 | endfunction
17 |
18 | :execute "normal gg"
19 | :execute 'g/\ 1:
14 | username = sys.argv[1]
15 | else:
16 | print("Usage: %s username" % (sys.argv[0],))
17 | sys.exit()
18 |
19 | scope = 'user-library-read'
20 | token = util.prompt_for_user_token(username, scope)
21 |
22 | if token:
23 | print("OL'YELLER")
24 | sp = spotipy.Spotify(auth=token)
25 | sp.trace = False
26 | saved_albums = str(sp.current_user_saved_albums(limit=50))
27 | j = int(int(saved_albums[saved_albums.rfind(' ')+1:len(saved_albums)-1])/50)+1 #total saved albums
28 | jsone = json.JSONEncoder()
29 | for i in range(0,j):
30 | results = sp.current_user_saved_albums(limit=50,offset=(50*i))
31 | file_name = "album_data" + str(i) + ".json"
32 | save_path = 'data/saved_albums/'
33 | completeName = os.path.join(save_path, file_name)
34 | with open(completeName, 'w') as f:
35 | json.dump(results,f)
36 | print(completeName+" was created successfully.")
37 | else:
38 | print("Can't get token for", username)
39 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/aggregate_saved_albums.js:
--------------------------------------------------------------------------------
1 | db = connect("localhost:27017/discogs_masters");
2 |
3 | var docs = db.temp.aggregate([{$unwind: {path: "$items"}}, {$project: {"_id": 0}}])
4 | db.temp2.insert(docs.toArray())
5 |
6 | if (db.current_albums.count() > 0) {
7 | print('db.current_albums.count()>0')
8 | // var current = db.current_albums.find()
9 | // var temp2 = db.temp2.find()
10 |
11 | db.current_albums.find().forEach(function(document){
12 | // print(document)
13 | try {
14 | var current_artist = document.items.album.artists[0].name
15 | var current_album = document.items.album.name
16 | var current_id = document.items.album.id
17 | // print(current_id)
18 | if (db.temp2.find({"items.album.id": current_id}).toArray().length == 0 ) {
19 | db.current_albums.deleteOne({"items.album.id": current_id})
20 | print("deleteOne")
21 | }
22 | } catch(e){
23 | print("does not contain album delete")
24 | }
25 | })
26 |
27 | print("pass")
28 |
29 | db.temp2.find().forEach(function(document){
30 | // print(document.toArray())
31 | try{
32 | // var temp2_id = document.items.album.id
33 | var temp2_artist = document.items.album.artists[0].name
34 | var temp2_album = document.items.album.name
35 | var temp2_id = document.items.album.id
36 |
37 | // print(temp2_artist+": "+temp2_album)
38 | if (db.current_albums.find({"items.album.id": temp2_id}).toArray().length == 0 ) {
39 | db.current_albums.insert(document)
40 | print("insert")
41 | }
42 | } catch(e){
43 | print("does not contain album insert")
44 | }
45 | })
46 |
47 | } else {
48 | db.current_albums.insert(docs.toArray())
49 | }
50 | db.temp2.drop()
51 | db.temp.drop()
52 | print("current_albums contains "+db.current_albums.count()+" albums.")
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | asn1crypto==0.24.0
2 | attrs==19.1.0
3 | Automat==0.7.0
4 | bleach==3.1.0
5 | blinker==1.4
6 | certifi==2019.6.16
7 | cffi==1.12.3
8 | chardet==3.0.4
9 | click==6.7
10 | cloud-init==18.5
11 | colorama==0.3.7
12 | command-not-found==0.3
13 | configobj==5.0.6
14 | constantly==15.1.0
15 | cryptography==2.7
16 | -e git+https://github.com/savoy1211/discogs-cli.git@cf0eb27283ea0e41cd61423f161c663df52d90e5#egg=discogs_cli
17 | discogs-cli-savoy1211==1.1.0
18 | discogs-client==2.2.1
19 | distro-info===0.18ubuntu0.18.04.1
20 | docutils==0.15.2
21 | example-pkg-verve3349==0.0.1
22 | httplib2==0.9.2
23 | hyperlink==19.0.0
24 | idna==2.8
25 | incremental==17.5.0
26 | jazz-lib==0.0.1
27 | Jinja2==2.10.1
28 | jsonpatch==1.16
29 | jsonpointer==1.10
30 | jsonschema==2.6.0
31 | keyring==10.6.0
32 | keyrings.alt==3.0
33 | language-selector==0.1
34 | MarkupSafe==1.1.1
35 | netifaces==0.10.4
36 | oauthlib==3.1.0
37 | olefile==0.45.1
38 | PAM==0.4.2
39 | Pillow==5.1.0
40 | pkginfo==1.5.0.1
41 | prompt-toolkit==2.0.6
42 | pyasn1==0.4.2
43 | pyasn1-modules==0.2.1
44 | pycparser==2.19
45 | pycrypto==2.6.1
46 | Pygments==2.4.2
47 | pygobject==3.26.1
48 | PyHamcrest==1.9.0
49 | PyJWT==1.5.3
50 | pylast==3.2.0.dev0
51 | pymongo==3.9.0
52 | pyOpenSSL==17.5.0
53 | pyserial==3.4
54 | python-apt==1.6.4
55 | python-debian==0.1.32
56 | pyxdg==0.26
57 | PyYAML==5.1.2
58 | readme-renderer==24.0
59 | requests==2.22.0
60 | requests-toolbelt==0.9.1
61 | requests-unixsocket==0.1.5
62 | SecretStorage==2.3.1
63 | service-identity==16.0.0
64 | simplejson==3.16.0
65 | six==1.12.0
66 | -e git+https://github.com/savoy1211/spotipy.git@b7b4f3128e8ea4608adc1b3d5d4a1ffa9ac810a0#egg=spotipy
67 | ssh-import-id==5.7
68 | systemd-python==234
69 | tqdm==4.35.0
70 | twine==1.13.0
71 | Twisted==19.7.0
72 | ufw==0.36
73 | unattended-upgrades==0.1
74 | urllib3==1.25.3
75 | wcwidth==0.1.7
76 | webencodings==0.5.1
77 | zope.interface==4.6.0
78 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/genres.js:
--------------------------------------------------------------------------------
1 | db = connect("localhost:27017/discogs_masters");
2 |
3 | var total_albums = db.current_albums.count()
4 | var i = 1
5 | var found = 0
6 | print(db.current_albums.find({"items.album.genres":[]}).count() +" documents with empty genre data")
7 | db.current_albums.find({"items.album.genres":[]}).forEach(function(document){
8 | // if ( document.items.album.genres == null ) {
9 | // print("This document is empty: "+document.items.album.genres)
10 | var id = document._id
11 | var artist = document.items.album.artists[0].name;
12 | var album = document.items.album.name;
13 | // print(artist+ ", "+album+"\n")
14 |
15 | if (album.length >= 5){
16 | album = album.substring(0,5)
17 | }
18 |
19 | var discogs_doc = db.current_masters.findOne({"masters.master.artists.artist.name": {$regex: artist, $options: "i" }, "masters.master.title": {$regex: album, $options: "i" }});
20 | if (discogs_doc != null) {
21 | if (typeof discogs_doc.masters.master.genres !== "undefined"){
22 | var genres = discogs_doc.masters.master.genres.genre;
23 | document.items.album.genres = genres;
24 | if (typeof discogs_doc.masters.master.styles !== "undefined" ){
25 | var styles = discogs_doc.masters.master.styles.style;
26 | document.items.album.styles = styles;
27 | }
28 | }
29 | var year = discogs_doc.masters.master.year;
30 | document.items.album.release_year = year;
31 | db.current_albums.update( {"_id": id} , document)
32 | print("+"+("("+i+"/"+total_albums+") ")+document.items.album.name + " by " + artist + " was found.");
33 | found++
34 | } else {
35 | db.current_albums_DIRTY.insert(document)
36 | print("-"+("("+i+"/"+total_albums+") ")+document.items.album.name + " by " + artist + " was null.");
37 | }
38 | i++
39 | print(i)
40 | // }
41 |
42 | })
43 |
44 |
45 |
46 | print(found+"/"+total_albums+" albums were found.")
47 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/get_genres.py:
--------------------------------------------------------------------------------
1 | import pymongo
2 | import discogs_client
3 | import time
4 | from sys import stdout
5 |
6 | # t_end = time.time() + 60 * 15
7 |
8 | client = pymongo.MongoClient("localhost", 27017)
9 | db = client.discogs_masters
10 | d = discogs_client.Client('discogs-spotify/0.1', user_token="VhFxDZnEzFcBLdTCSnsGfAmxgHmCOYxXMSdbOHci")
11 |
12 | total_albums = db.current_albums.count()
13 | i = 1
14 | found = 0
15 | albums = db.current_albums.find({"items.album.genres":[]})
16 | print(str(albums.count()) +" documents with empty genre data")
17 |
18 | for document in albums:
19 | db_id = document["_id"]
20 | db_artist = document["items"]["album"]["artists"][0]["name"]
21 | db_album = document["items"]["album"]["name"]
22 | query = db_artist+" "+db_album
23 | # if (album.length >= 5):
24 | # album = album.substring(0,6)
25 | try:
26 | master = d.search(query, type="release")[0].master
27 | # print(master)
28 | document["items"]["album"]["name"] = master.title
29 | document["items"]["album"]["artists"][0]["name"] = master.main_release.artists[0].name
30 | document["items"]["album"]["genres"] = master.genres
31 | document["items"]["album"]["styles"] = master.styles
32 | document["items"]["album"]["label"] = master.main_release.labels[0].name
33 | document["items"]["album"]["release_year"] = master.main_release.year
34 | print("+"+("("+str(i)+"/"+str(total_albums)+") ")+db_album + " by " + db_artist + " was found.")
35 |
36 | db.current_albums.update( {"_id": db_id} , document)
37 | found += 1
38 |
39 | except discogs_client.exceptions.HTTPError:
40 | # albums.append(document)
41 | t_end = time.time() + 30
42 | while time.time() < t_end:
43 | # print('waiting '+str(int(t_end)))
44 | denom = '/'+str(int(t_end))
45 | total_time = int(t_end) - int(time.time())
46 |
47 | stdout.write("\rWait %d" % total_time+"s...")
48 | stdout.flush()
49 | stdout.write("\n")
50 |
51 | except Exception:
52 | # print(Exception)
53 | print("-"+("("+str(i)+"/"+str(total_albums)+") ")+db_album + " by " + db_artist + " was null.");
54 | i+=1
55 |
56 |
57 | print(str(found)+"/"+str(total_albums)+" albums were found.")
58 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/discogs_search_update.py:
--------------------------------------------------------------------------------
1 | """
2 | Update album data by searching with Discogs Python client.
3 |
4 | """
5 |
6 | import pymongo
7 | import discogs_client
8 |
9 | client = pymongo.MongoClient("localhost", 27017)
10 | db = client.discogs_masters
11 | current_albums = db.current_albums
12 |
13 | d = discogs_client.Client('discogs-spotify/0.1', user_token="VhFxDZnEzFcBLdTCSnsGfAmxgHmCOYxXMSdbOHci")
14 |
15 | while True:
16 | title = input("enter album title from db.current_albums: ")
17 | doc = db.current_albums.find({"items.album.name": title})
18 | search_title = input("enter album to search in Discogs: ")
19 | for album in doc:
20 | id = album["_id"]
21 | try:
22 |
23 | artist = album["items"]["album"]["artists"][0]["name"]
24 | label = album["items"]["album"]["label"]
25 | year = album["items"]["album"]["release_year"]
26 | genres = album["items"]["album"]["genres"]
27 | styles = album["items"]["album"]["styles"]
28 | except Exception:
29 | styles = []
30 | print("Here is the current data: ")
31 | print("label: "+str(label))
32 | print("year: "+str(year))
33 | print("genres: "+str(genres))
34 | print("styles: "+str(styles))
35 | print()
36 | results = d.search(search_title, type='master')
37 | choose_next = input("Is this the correct data [y/n]? ")
38 | i = 0
39 | while (choose_next != 'y'):
40 | print("artist: "+str(results[i].main_release.artists[0].name))
41 | print("label: "+str(results[i].main_release.labels[0].name))
42 | print("year: "+str(results[i].main_release.year))
43 | print("genres: "+str(results[i].genres))
44 | print("styles: "+str(results[i].styles))
45 | choose_next = input("Is this the correct data [y/n]? ")
46 | if (choose_next == "n"):
47 | i+=1
48 |
49 |
50 | album["items"]["album"]["release_year"] = results[0].main_release.year
51 | album["items"]["album"]["genres"] = results[0].genres
52 | album["items"]["album"]["styles"] = results[0].styles
53 | album["items"]["album"]["label"] = results[0].main_release.labels[0].name
54 |
55 | db.current_albums.update({"_id": id}, album)
56 | print()
57 | print("--Updated data--")
58 | print("label: "+str(album["items"]["album"]["label"]))
59 | print("year: "+str(album["items"]["album"]["release_year"]))
60 | print("genres: "+str(album["items"]["album"]["genres"]))
61 | print("styles: "+str(album["items"]["album"]["styles"]))
62 | print()
63 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/clean_data.py:
--------------------------------------------------------------------------------
1 | """
2 | Cleans album data by updating parameters (label, release year, genres, styles).
3 |
4 | """
5 |
6 | import os
7 | import pymongo
8 | from sys import stdout
9 |
10 | class cleanData:
11 |
12 | def inputParam(parameter,document):
13 | """
14 | return updated document
15 |
16 | param: album parameter data such as genres, styles, and label
17 |
18 | """
19 | if (parameter == "genres" or parameter == "styles"):
20 | genres_array = input("How many "+parameter+"? ")
21 | if (int(genres_array) > 1):
22 | number = int(genres_array)
23 | genres = []
24 | for i in range(0, number):
25 | genre = input(parameter+": ")
26 | genres.append(genre)
27 | document["items"]["album"][parameter] = genres
28 | elif (int(genres_array) == 1):
29 | genre = input(parameter+": ")
30 | document["items"]["album"][parameter] = genre
31 | else:
32 | genre = input(parameter+": ")
33 | document["items"]["album"][parameter] = genre
34 |
35 | return document
36 |
37 |
38 | client = pymongo.MongoClient("localhost", 27017)
39 | db = client.discogs_masters
40 |
41 | next_album = ''
42 | while(next_album != "n"):
43 | album_name = input("Enter album name: ")
44 |
45 | doc = db.current_albums.find({"items.album.name": album_name})
46 | for album in doc:
47 | try:
48 | id = album["_id"]
49 | artist = album["items"]["album"]["artists"][0]["name"]
50 | label = album["items"]["album"]["label"]
51 | year = album["items"]["album"]["release_year"]
52 | genres = album["items"]["album"]["genres"]
53 | styles = album["items"]["album"]["styles"]
54 | except Exception:
55 | styles = []
56 |
57 |
58 | print("Title: "+album_name)
59 | print("Artist: "+artist)
60 | print("Label: "+label)
61 | print("Year: "+year)
62 | print("Genre(s): "+str(genres))
63 | print("Style(s): "+str(styles))
64 |
65 | parameter = ''
66 | while (parameter != "exit"):
67 | parameter = input("Select parameter to update ([label], [release_year], [genres], [styles], exit): ")
68 | updates = ""
69 |
70 | # if (parameter == "genres" or parameter == "styles"):
71 | # # print("If more than one, use array brackets []")
72 | # inputParam(parameter,album)
73 | # # updates = input("Input updates for "+parameter+": ")
74 | # in
75 | # else:
76 | # updates = input("Input updates for "+parameter+": ")
77 |
78 |
79 | # print(id)
80 | if (parameter != "exit"):
81 | inputParam(parameter,album)
82 | # album["items"]["album"][parameter] = updates
83 | db.current_albums.update({"_id": id}, album)
84 |
85 |
86 | next_album = input("Update another [y/n]? ")
87 |
88 | print("Goodbye.")
89 |
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spotify Albums Organizer
2 |
3 | 
4 |
5 | This app lets you organize and visualize your saved Spotify albums. You can sort by genre, year, date added, etc.
6 |
7 | Import your saved albums from Spotify and the monthly Discogs masters data -- ~1.5GB -- into MongoDB. Since Spotify data lacks information about genre and subgenres (styles), discogs-spotify-import retrieves genre and subgenre data from the Discogs library and updates the Spotify data.
8 |
9 | **This app only works on Python 3.7+ due to restriction on Tkinter**
10 |
11 | Download MongoDB if needed from
12 | If you're on Windows, make sure to download Xming
13 | ## Specifications
14 | The Mongo database uses localhost: 27017
15 |
16 | * Database name: **discogs_masters**
17 |
18 | * Collections: **current_albums** (saved Spotify albums) **current_masters** (Discogs masters).
19 |
20 | ## Dependencies
21 |
22 | $ pip3 install -r requirements.txt
23 |
24 | ## Usage
25 |
26 | ### 1. Import album data
27 |
28 | $ cd spotify-albums-organizer
29 | $ chmod +x run.sh
30 | $ ./run.sh [spotify-id] [discogs-masters-xml-file]
31 |
32 | * Find your Spotify id on your browser's tab when you go to
33 | * Download the XML Discogs masters data into the spotify-albums-organizer directory from . Importing others may be buggy.
34 |
35 | **This will take some time. On my 2015 MacBook Air, this step took ~1hr.**
36 |
37 | ### 1.5 Import album data
38 |
39 | Additionally, search Discogs data using Discogs' Python client. This bypasses adding the masters data into Mongo.
40 |
41 | $ cd spotify-albums-organizer
42 | $ chmod +x run_quicker.sh
43 | $ ./run_quicker.sh [spotify-id]
44 |
45 | Or, if saved Spotify albums already exist in Mongo:
46 |
47 | $ python3 get_genres.py
48 |
49 | ### 2. Import album cover images
50 |
51 | $ python3 import_images.py
52 |
53 | ### 3. Clean data
54 |
55 | **There will likely be incomplete album data.** Consider cleaning your data for more accurate data (i.e release year, label, etc.):
56 |
57 | #### Example:
58 |
59 | #### `clean_data.py`
60 |
61 | Cleans album data by updating parameters (label, release year, genres, styles).
62 |
63 | $ python3 clean_data.py
64 | $ Enter album name: Moondog
65 | $ Title: Moondog
66 | $ Artist: Moondog
67 | $ Label: Columbia Masterworks
68 | $ Year: 1969
69 | $ Genre(s): ['Jazz', 'Classical']
70 | $ Style(s): ['Big Band', 'Contemporary']
71 | $ Select parameter to update ([label], [release_year], [genres], [styles], exit): label
72 | $ label: CBS
73 |
74 | #### `show_empty_genre_albums.py`
75 |
76 | Prints albums (album title and artist) with empty genre data.
77 |
78 | $ python3 show_empty_genre_albums.py
79 | 1. title: Mr Finish Line
80 | artist: Vulfpeck
81 | 2. title: COGNITIO
82 | artist: Yokaze
83 | 3. title: Rhapsody In White (Reissue)
84 | artist: The Love Unlimited Orchestra
85 |
86 | #### `discogs_search_update.py`
87 |
88 | Update album data by searching with Discogs Python client.
89 |
90 | $ python3 discogs_search_update.py
91 | enter album to search in Discogs: Rhapsody In White
92 | Here is the current data:
93 | label: Mercury Records
94 | year: 1998
95 | genres: []
96 | styles: []
97 |
98 | Is this the correct data [y/n]? n
99 | artist: Love Unlimited Orchestra
100 | label: 20th Century Records
101 | year: 1974
102 | genres: ['Jazz', 'Funk / Soul']
103 | styles: ['Soul-Jazz', 'Jazz-Funk', 'Soul', 'Disco']
104 | Is this the correct data [y/n]? y
105 |
106 | ### 4. Display saved Spotify albums
107 |
108 | #### Example:
109 |
110 | #### `display_albums.py`
111 |
112 | For the windows users, if you haven't already, make sure you are running XMing. This method displays saved Spotify albums in a GUI. Click on an album cover to open album's Spotify page. The search buttons allow you to query the artist name and album title in the Discogs or RateYourMusic search bars.
113 |
114 | $ export DISPLAY=:0;
115 | $ python3 display_albums.py
116 | $ Input is case-sensitive!
117 | $ choose genre([Electronic], [Jazz], [Rock], [Classical], [Funk / Soul], [Hip Hop], all): Jazz
118 | $ choose sorting option([release_year], [name], [artists[0].name], [label], [genres], [added_at]): release_year
119 | $ choose direction([ascending], [descending]): descending
120 | $ Loading 194/219 albums...
121 |
122 | In the GUI, click on an album cover to go to its Spotify page. You may customize the number of columns by changing "cols" in `display_albums.py` (line 93).
123 |
124 | ### Updating saved albums
125 |
126 | If you want to update your saved album data (after you've added or deleted any saved albums from the Spotify app), run these commands:
127 |
128 | $ chmod +x update_saved_albums.sh
129 | $ ./update_saved_albums.sh [spotify-id]
130 |
131 |
132 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/display_albums.py:
--------------------------------------------------------------------------------
1 | """
2 | Displays saved Spotify albums in a GUI. Click on an album cover to open album's Spotify page.
3 |
4 | """
5 |
6 | import tkinter as tk
7 | from PIL import ImageTk, Image
8 | import os
9 | import requests
10 | from io import BytesIO
11 | from sys import stdout
12 | import pymongo
13 | from functools import partial
14 | import webbrowser
15 |
16 | def go_to(url):
17 | """
18 | Opens the Spotify album's URL in open.spotify.com
19 |
20 | """
21 | webbrowser.open(url)
22 |
23 | def onFrameConfigure(canvas):
24 | """
25 | Reset the scroll region to encompass the inner frame
26 |
27 | """
28 | canvas.configure(scrollregion=canvas.bbox("all"))
29 |
30 | def truncate(param):
31 | """
32 | return a substring of param to fit column grid
33 |
34 | :param: album data parameter such as label, year, and album title
35 |
36 | """
37 | max = 21
38 | if (len(param) > max):
39 | param = param[:max]
40 | return param
41 |
42 | """
43 | Select sorting options
44 |
45 | """
46 | client = pymongo.MongoClient("localhost", 27017)
47 | db = client.discogs_masters
48 | print("Input is case-sensitive!")
49 | genre = input("choose genre([Electronic], [Jazz], [Rock], [Classical], [Funk / Soul], [Hip Hop], all): ")
50 | param = input("choose sorting option([release_year], [name], [artists[0].name], [label], [genres], [added_at]): ")
51 | sort_option = "items.album."+param
52 | direction = input("choose direction([ascending], [descending]): ")
53 |
54 | """
55 | Check if release_year type is string. Update if necessary.
56 | Capitalize artist name for sort.
57 |
58 | """
59 | albums = db.current_albums.find()
60 | for album in albums:
61 | try:
62 | id = album["_id"]
63 | year = album["items"]["album"]["release_year"]
64 | year = str(year)
65 | album["items"]["album"]["release_year"] = year
66 | # album["items"]["album"]["artists"][0]["name"] = album["items"]["album"]["artists"][0]["name"].capitalize()
67 | db.current_albums.update({"_id": id}, album)
68 | except Exception:
69 | j = 0
70 |
71 |
72 | select_albums = 0
73 | if (direction == "ascending" and param == 'artists.name'):
74 | select_albums = db.current_albums.find({"items.album.genres": genre}).sort([(sort_option, pymongo.ASCENDING), ("items.album.release_year", pymongo.DESCENDING)])
75 | elif (direction == "descending" and param == 'artists.name'):
76 | select_albums = db.current_albums.find({"items.album.genres": genre}).sort([(sort_option, pymongo.ASCENDING), ("items.album.release_year", pymongo.DESCENDING)])
77 | elif (direction == "ascending" and param == 'label'):
78 | select_albums = db.current_albums.find({"items.album.genres": genre}).sort([(sort_option, pymongo.ASCENDING), ("items.album.release_year", pymongo.DESCENDING)])
79 | elif (direction == "descending" and param == 'label'):
80 | select_albums = db.current_albums.find({"items.album.genres": genre}).sort([(sort_option, pymongo.ASCENDING), ("items.album.release_year", pymongo.DESCENDING)])
81 | elif (direction == "ascending" and genre != 'all'):
82 | select_albums = db.current_albums.find({"items.album.genres": genre}).sort(sort_option, pymongo.ASCENDING)
83 | elif (direction == "descending" and genre != 'all'):
84 | select_albums = db.current_albums.find({"items.album.genres": genre}).sort(sort_option, pymongo.DESCENDING)
85 | elif (direction == "ascending" and genre == 'all'):
86 | select_albums = db.current_albums.find().sort(sort_option, pymongo.ASCENDING)
87 | elif (direction == "descending" and genre == 'all'):
88 | select_albums = db.current_albums.find().sort(sort_option, pymongo.DESCENDING)
89 |
90 | """
91 | Retrieve album data from db.current_albums
92 |
93 | """
94 | photos = []
95 | object_ids = []
96 | ids = []
97 | names = []
98 | artists = []
99 | labels = []
100 | years = []
101 | genres = []
102 | urls = []
103 |
104 | for album in select_albums:
105 | try:
106 | object_id = album["_id"]
107 | id = album["items"]["album"]["id"]
108 | name = album["items"]["album"]["name"]
109 | artist = album["items"]["album"]["artists"][0]["name"]
110 | label = album["items"]["album"]["label"]
111 | year = album["items"]["album"]["release_year"]
112 | genre = album["items"]["album"]["genres"]
113 | url = album["items"]["album"]["external_urls"]["spotify"]
114 |
115 | object_ids.append(object_id)
116 | ids.append(id)
117 | names.append(name)
118 | artists.append(artist)
119 | labels.append(label)
120 | years.append(year)
121 | genres.append(genre)
122 | urls.append(url)
123 | except Exception:
124 | print(album["items"]["album"]["name"]+" doesn't work.")
125 |
126 | #"""
127 | # Create tkinter canvas
128 | #
129 | #"""
130 | root = tk.Tk()
131 | canvas = tk.Canvas(root, borderwidth=0, background="#ffffff", width=1440, height=800)
132 | frame = tk.Frame(canvas, background="#f8f8ff")
133 | vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
134 | canvas.configure(yscrollcommand=vsb.set)
135 | vsb.pack(side="right", fill="y")
136 | canvas.pack(side="left", fill="both", expand=True)
137 | canvas.create_window((15,15), window=frame, anchor="nw")
138 | frame.bind("", lambda event, canvas=canvas: onFrameConfigure(canvas))
139 |
140 |
141 | i=0
142 | cols = 8
143 | a = int(len(ids)/cols)+1
144 |
145 | for r in range(a):
146 | for c in range(cols):
147 | try:
148 | filename = ids[i]
149 | img = ImageTk.PhotoImage(Image.open("data/images/"+filename).resize((150, 150), Image.ANTIALIAS))
150 | photos.append(img)
151 | name = truncate(names[i])
152 | artist = truncate(artists[i])
153 | label = truncate(labels[i])
154 | url = urls[i]
155 | query = str(artist +" "+ name)
156 | query = query.replace(" ","+")
157 | search_url_RYM = "https://rateyourmusic.com/search?searchtype=l"+'"&"'+"searchterm="+query
158 | search_url_discogs = "https://www.discogs.com/search/?q="+query+'"&"'+"type=master"
159 |
160 | # """
161 | # Display album image. Hyperlink to album's Spotify page.
162 | #
163 | # """
164 | tk.Button(frame,image=photos[i] ,command=partial(go_to, url)).grid(row=6*r,column=c)
165 |
166 | """
167 | Display album name and artist.
168 |
169 | """
170 | # print()
171 | # print(artist)
172 | # print(name)
173 |
174 | tk.Label(frame, text=str(artist)).grid(row=(6*r)+1, column=c)
175 | tk.Label(frame, text=str(name)).grid(row=(6*r)+2, column=c)
176 |
177 | """
178 | Display album ObjectId (selectable)
179 |
180 | """
181 | # v = tk.StringVar()
182 | # w = tk.Entry(frame,textvariable=v,fg="black",bg="white",bd=0,state="readonly").grid(row=(4*r)+1, column=c)
183 | # v.set(object_ids[i])
184 |
185 | """
186 | Display album label and year.
187 |
188 | """
189 | # print(label)
190 | # print(years[i])
191 | tk.Label(frame, text=str(label[:13])+", " +years[i]).grid(row=(6*r)+3, column=c)
192 |
193 | """
194 | Add RYM and Discgos search buttons
195 |
196 | """
197 | # print(search_url_discogs)
198 | # print(search_url_RYM)
199 | # print()
200 |
201 | tk.Button(frame,text="Search Discogs", command=partial(go_to, search_url_discogs)).grid(row=(6*r)+4,column=c)
202 | tk.Button(frame,text="Search RYM", command=partial(go_to, search_url_RYM)).grid(row=(6*r)+5,column=c)
203 |
204 | i+=1
205 |
206 | """
207 | Display albums loading e.g. 5/112
208 |
209 | """
210 | denom = '/'+str(len(ids))
211 | stdout.write("\rLoading %d" % i +denom+" albums...")
212 | stdout.flush()
213 |
214 | except Exception:
215 | break
216 |
217 | stdout.write("\n")
218 | root.mainloop( )
219 |
--------------------------------------------------------------------------------
/spotify-albums-organizer/xml2json.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """xml2json.py Convert XML to JSON
4 |
5 | Relies on ElementTree for the XML parsing. This is based on
6 | pesterfish.py but uses a different XML->JSON mapping.
7 | The XML->JSON mapping is described at
8 | http://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html
9 |
10 | Rewritten to a command line utility by Hay Kranen < github.com/hay > with
11 | contributions from George Hamilton (gmh04) and Dan Brown (jdanbrown)
12 |
13 | XML JSON
14 | "e": null
15 | text "e": "text"
16 | "e": { "@name": "value" }
17 | text "e": { "@name": "value", "#text": "text" }
18 | texttext "e": { "a": "text", "b": "text" }
19 | text text "e": { "a": ["text", "text"] }
20 | text text "e": { "#text": "text", "a": "text" }
21 |
22 | This is very similar to the mapping used for Yahoo Web Services
23 | (http://developer.yahoo.com/common/json.html#xml).
24 |
25 | This is a mess in that it is so unpredictable -- it requires lots of testing
26 | (e.g. to see if values are lists or strings or dictionaries). For use
27 | in Python this could be vastly cleaner. Think about whether the internal
28 | form can be more self-consistent while maintaining good external
29 | characteristics for the JSON.
30 |
31 | Look at the Yahoo version closely to see how it works. Maybe can adopt
32 | that completely if it makes more sense...
33 |
34 | R. White, 2006 November 6
35 | """
36 |
37 | import json
38 | import optparse
39 | import sys
40 | import os
41 | from collections import OrderedDict
42 |
43 | import xml.etree.cElementTree as ET
44 |
45 |
46 | def strip_tag(tag):
47 | strip_ns_tag = tag
48 | split_array = tag.split('}')
49 | if len(split_array) > 1:
50 | strip_ns_tag = split_array[1]
51 | tag = strip_ns_tag
52 | return tag
53 |
54 |
55 | def elem_to_internal(elem, strip_ns=1, strip=1):
56 | """Convert an Element into an internal dictionary (not JSON!)."""
57 |
58 | d = OrderedDict()
59 | elem_tag = elem.tag
60 | if strip_ns:
61 | elem_tag = strip_tag(elem.tag)
62 | for key, value in list(elem.attrib.items()):
63 | d['@' + key] = value
64 |
65 | # loop over subelements to merge them
66 | for subelem in elem:
67 | v = elem_to_internal(subelem, strip_ns=strip_ns, strip=strip)
68 |
69 | tag = subelem.tag
70 | if strip_ns:
71 | tag = strip_tag(subelem.tag)
72 |
73 | value = v[tag]
74 |
75 | try:
76 | # add to existing list for this tag
77 | d[tag].append(value)
78 | except AttributeError:
79 | # turn existing entry into a list
80 | d[tag] = [d[tag], value]
81 | except KeyError:
82 | # add a new non-list entry
83 | d[tag] = value
84 | text = elem.text
85 | tail = elem.tail
86 | if strip:
87 | # ignore leading and trailing whitespace
88 | if text:
89 | text = text.strip()
90 | if tail:
91 | tail = tail.strip()
92 |
93 | if tail:
94 | d['#tail'] = tail
95 |
96 | if d:
97 | # use #text element if other attributes exist
98 | if text:
99 | d["#text"] = text
100 | else:
101 | # text is the value if no attributes
102 | d = text or None
103 | return {elem_tag: d}
104 |
105 |
106 | def internal_to_elem(pfsh, factory=ET.Element):
107 |
108 | """Convert an internal dictionary (not JSON!) into an Element.
109 |
110 | Whatever Element implementation we could import will be
111 | used by default; if you want to use something else, pass the
112 | Element class as the factory parameter.
113 | """
114 |
115 | attribs = OrderedDict()
116 | text = None
117 | tail = None
118 | sublist = []
119 | tag = list(pfsh.keys())
120 | if len(tag) != 1:
121 | raise ValueError("Illegal structure with multiple tags: %s" % tag)
122 | tag = tag[0]
123 | value = pfsh[tag]
124 | if isinstance(value, dict):
125 | for k, v in list(value.items()):
126 | if k[:1] == "@":
127 | attribs[k[1:]] = v
128 | elif k == "#text":
129 | text = v
130 | elif k == "#tail":
131 | tail = v
132 | elif isinstance(v, list):
133 | for v2 in v:
134 | sublist.append(internal_to_elem({k: v2}, factory=factory))
135 | else:
136 | sublist.append(internal_to_elem({k: v}, factory=factory))
137 | else:
138 | text = value
139 | e = factory(tag, attribs)
140 | for sub in sublist:
141 | e.append(sub)
142 | e.text = text
143 | e.tail = tail
144 | return e
145 |
146 |
147 | def elem2json(elem, options, strip_ns=1, strip=1):
148 |
149 | """Convert an ElementTree or Element into a JSON string."""
150 |
151 | if hasattr(elem, 'getroot'):
152 | elem = elem.getroot()
153 |
154 | if options.pretty:
155 | return json.dumps(elem_to_internal(elem, strip_ns=strip_ns, strip=strip), indent=4, separators=(',', ': '))
156 | else:
157 | return json.dumps(elem_to_internal(elem, strip_ns=strip_ns, strip=strip))
158 |
159 |
160 | def json2elem(json_data, factory=ET.Element):
161 |
162 | """Convert a JSON string into an Element.
163 |
164 | Whatever Element implementation we could import will be used by
165 | default; if you want to use something else, pass the Element class
166 | as the factory parameter.
167 | """
168 |
169 | return internal_to_elem(json.loads(json_data), factory)
170 |
171 |
172 | def xml2json(xmlstring, options, strip_ns=1, strip=1):
173 |
174 | """Convert an XML string into a JSON string."""
175 | try:
176 | elem = ET.fromstring(xmlstring)
177 | except Exception:
178 | a = 1
179 | else:
180 | return elem2json(elem, options, strip_ns=strip_ns, strip=strip)
181 |
182 |
183 | def json2xml(json_data, factory=ET.Element):
184 |
185 | """Convert a JSON string into an XML string.
186 |
187 | Whatever Element implementation we could import will be used by
188 | default; if you want to use something else, pass the Element class
189 | as the factory parameter.
190 | """
191 | if not isinstance(json_data, dict):
192 | json_data = json.loads(json_data)
193 |
194 | elem = internal_to_elem(json_data, factory)
195 | return ET.tostring(elem)
196 |
197 |
198 | def main():
199 | p = optparse.OptionParser(
200 | description='Converts XML to JSON or the other way around. Reads from standard input by default, or from file if given.',
201 | prog='xml2json',
202 | usage='%prog -t xml2json -o file.json [file]'
203 | )
204 | p.add_option('--type', '-t', help="'xml2json' or 'json2xml'", default="xml2json")
205 | p.add_option('--out', '-o', help="Write to OUT instead of stdout")
206 | p.add_option(
207 | '--strip_text', action="store_true",
208 | dest="strip_text", help="Strip text for xml2json")
209 | p.add_option(
210 | '--pretty', action="store_true",
211 | dest="pretty", help="Format JSON output so it is easier to read")
212 | p.add_option(
213 | '--strip_namespace', action="store_true",
214 | dest="strip_ns", help="Strip namespace for xml2json")
215 | p.add_option(
216 | '--strip_newlines', action="store_true",
217 | dest="strip_nl", help="Strip newlines for xml2json")
218 | options, arguments = p.parse_args()
219 |
220 | inputstream = sys.stdin
221 | if len(arguments) == 1:
222 | try:
223 | inputstream = open(arguments[0])
224 | except:
225 | sys.stderr.write("Problem reading '{0}'\n".format(arguments[0]))
226 | p.print_help()
227 | sys.exit(-1)
228 |
229 | input = inputstream.read()
230 |
231 | strip = 0
232 | strip_ns = 0
233 | if options.strip_text:
234 | strip = 1
235 | if options.strip_ns:
236 | strip_ns = 1
237 | if options.strip_nl:
238 | input = input.replace('\n', '').replace('\r','')
239 | if (options.type == "xml2json"):
240 | out = xml2json(input, options, strip_ns, strip)
241 | else:
242 | out = json2xml(input)
243 |
244 | if (options.out and out != None):
245 | file = open(options.out, 'w')
246 | file.write(out)
247 | file.close()
248 | else:
249 | print("fix: "+str(arguments))
250 |
251 | if __name__ == "__main__":
252 | main()
253 |
254 |
--------------------------------------------------------------------------------