├── 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 | ![GUI example](https://raw.githubusercontent.com/savoy1211/spotify-albums-organizer/master/display_covers.png) 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 | --------------------------------------------------------------------------------