├── .gitignore ├── src ├── .DS_Store ├── html │ └── .DS_Store └── js │ └── validation.js ├── python_versoin ├── .DS_Store ├── images │ ├── app_page.png │ ├── get_id_sec.png │ ├── cretae_an_app.png │ └── create_an_app_Detail.png ├── utf8_trans.py ├── log ├── Search_Artists_Song_Spotify.py ├── crawler.js ├── README.md ├── main.py └── Login_Fetch_Xiami.py ├── log ├── pw.js ├── LICENSE ├── package.json ├── db.js ├── views └── index.hbs ├── README.md ├── NetEaseCloudMusic.js ├── test.js ├── Xiami.js ├── main.js └── Spotify.js /.gitignore: -------------------------------------------------------------------------------- 1 | account.js 2 | .vscode/ 3 | node_modules/ 4 | *Old.js 5 | pw.js -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/html/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/src/html/.DS_Store -------------------------------------------------------------------------------- /python_versoin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/python_versoin/.DS_Store -------------------------------------------------------------------------------- /python_versoin/images/app_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/python_versoin/images/app_page.png -------------------------------------------------------------------------------- /python_versoin/images/get_id_sec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/python_versoin/images/get_id_sec.png -------------------------------------------------------------------------------- /python_versoin/images/cretae_an_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/python_versoin/images/cretae_an_app.png -------------------------------------------------------------------------------- /python_versoin/images/create_an_app_Detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhang435/Music-Porter/HEAD/python_versoin/images/create_an_app_Detail.png -------------------------------------------------------------------------------- /log: -------------------------------------------------------------------------------- 1 | 05/14/2019: improve netease parser so user can find input url with &userid at the end 2 | 02/26/2019: disable submit button after user click submit once 3 | 02/26/2019: add google Aanlytic into index page 4 | -------------------------------------------------------------------------------- /pw.js: -------------------------------------------------------------------------------- 1 | var DB_CONNECTION_LOCAL = { 2 | user: "xplywmiccxoqla", 3 | password: "0a116ba1d118df5e9a21b651a479f6ad637d775ed08bc0ca7c5abdb08aba92a3", 4 | database: "db048t0ojvr0hg", 5 | host: "ec2-23-21-244-254.compute-1.amazonaws.com", 6 | port: 5432, 7 | ssl: true 8 | }; 9 | 10 | var DB_CONNECTION_SERVER = { 11 | connectionString: process.env.DATABASE_URL, 12 | ssl: true 13 | }; 14 | module.exports = { 15 | DB_CONNECTION_LOCAL, 16 | DB_CONNECTION_SERVER 17 | }; -------------------------------------------------------------------------------- /python_versoin/utf8_trans.py: -------------------------------------------------------------------------------- 1 | ''' 2 | this function enable user to search the song and artist name in chinese 3 | this is enable chinese ben tranfer into hex so it will pass into API 4 | ''' 5 | 6 | def to_utf8(text): 7 | if isinstance(text, unicode): 8 | # unicode to utf-8 9 | return text.encode('utf-8') 10 | try: 11 | # maybe utf-8 12 | return text.decode('utf-8').encode('utf-8') 13 | except UnicodeError: 14 | # gbk to utf-8 15 | return text.decode('gbk').encode('utf-8') 16 | 17 | 18 | def isEnglish(s): 19 | try: 20 | s.decode('ascii') 21 | except UnicodeDecodeError: 22 | return False 23 | else: 24 | return True 25 | -------------------------------------------------------------------------------- /python_versoin/log: -------------------------------------------------------------------------------- 1 | 01.19 2 | finished whole project 3 | 4 | 5 | problem: 6 | at this point, every function works, while the problem become 7 | the resources in spotify, for some chinese singer have simply name 8 | in spotify but some have tridional chinese in Spotify, some of them even have 9 | english name on it and wtf.... 10 | 11 | 12 | enable to get uset playlist from Spotify, find the fcuntion to add music into the spotify 13 | enable user to get their farviort from xiami 01/14 14 | 15 | 16 | xiami music 17 | login into xiami 18 | go to my songs 19 | grab the song name and player name 20 | 21 | go to spotify 22 | find the music 23 | if music not find 24 | tell user about it 25 | else: 26 | add to the playlist 27 | 28 | find the api for the add to playlist and download 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jiawei Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /python_versoin/Search_Artists_Song_Spotify.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import sys 3 | import os 4 | import base64 5 | import spotipy 6 | import spotipy.util as util 7 | from utf8_trans import to_utf8, isEnglish 8 | # encoding: utf-8 9 | # -*- coding: utf-8 -*- 10 | # this function is im purose to find the specific artist's song 11 | # specially spend time to deal with tranfer chinese to to uncoide and 12 | # search , change the uncoide backinto chinese 13 | 14 | scope = 'playlist-modify-public' 15 | sp = spotipy.Spotify() 16 | 17 | class Search_Singers_Song(object): 18 | 19 | def __init__(self, singer, song): 20 | self.singer = singer 21 | self.song = song 22 | 23 | def get_song_uri(self): 24 | global sp 25 | if not isEnglish(self.song): 26 | self.song = to_utf8(self.song) 27 | results = sp.search(q='track:' + self.song, type='track') 28 | items = results["tracks"]["items"] 29 | 30 | for item in items: 31 | # print item['artists'][0]['name'].encode('utf8', 'ignore').lower(), 32 | # print self.singer 33 | if item['artists'][0]['name'].encode('utf8', 'ignore').lower() == self.singer.lower(): 34 | return (True, item['artists'][0]['name'].encode('utf8', 'ignore').lower(), item['uri'], item['id']) 35 | return [False, self.song, self.singer] 36 | -------------------------------------------------------------------------------- /python_versoin/crawler.js: -------------------------------------------------------------------------------- 1 | const https = require("https"); 2 | const http = require("http"); 3 | const superagent = require("superagent"); 4 | const cheerio = require("cheerio"); 5 | var querystring = require("querystring"); 6 | var browserMsg={ 7 | "User-Agent" :"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36", 8 | 'Content-Type':'application/x-www-form-urlencoded', 9 | "referer" :'https://login.xiami.com/member/login' 10 | }; 11 | 12 | var log_in_url = "https://login.xiami.com/member/login"; 13 | var playlist_url = "http://www.xiami.com/space/lib-song/page/1"; 14 | 15 | 16 | 17 | superagent.get('https://login.xiami.com/member/login') 18 | .then((res) => { 19 | const xiamiToken = res.headers['set-cookie'][1].match(/_xiamitoken=(\w+);/)[1] 20 | 21 | const postData = { 22 | '_xiamitoken': xiamiToken, 23 | 'account': "apple19950105@gmail.com", 24 | 'pw': "apple19950105" 25 | } 26 | 27 | const options = { 28 | hostname: 'login.xiami.com', 29 | path: '/passport/login', 30 | method: 'POST', 31 | headers: { 32 | 'Referer': 'https://login.xiami.com/member/login', 33 | 'Cookie': `_xiamitoken=${xiamiToken}`, 34 | 'Content-Type': 'application/x-www-form-urlencoded' 35 | } 36 | } 37 | 38 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xiami_to_spotify", 3 | "version": "1.0.0", 4 | "description": "![\\](https://upload-images.jianshu.io/upload_images/4457561-dd78853d4dfed3ed.png)](https://upload-images.jianshu.io/upload_images/4457561-dd78853d4dfed3ed.png)", 5 | "main": "Spotify.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node main.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/zhang435/Xiami_To_Spotify.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/zhang435/Xiami_To_Spotify/issues" 18 | }, 19 | "homepage": "https://github.com/zhang435/Xiami_To_Spotify#readme", 20 | "dependencies": { 21 | "bluebird": "^3.5.3", 22 | "body-parser": "^1.18.3", 23 | "cheerio": "^1.0.0-rc.2", 24 | "crawler": "^1.2.0", 25 | "express": "^4.16.4", 26 | "grunt": "^1.0.3", 27 | "hbs": "^4.0.1", 28 | "jsdom": "^11.12.0", 29 | "lodash": "^4.17.15", 30 | "mongodb": "^3.1.13", 31 | "no": "0.0.1", 32 | "nodemon": "^1.18.9", 33 | "npm": "^6.13.4", 34 | "pg": "^7.8.0", 35 | "querystring": "^0.2.0", 36 | "redis": "^2.8.0", 37 | "request": "^2.88.0", 38 | "request-promise": "^4.2.2", 39 | "superagent": "^3.8.3", 40 | "url": "^0.11.0", 41 | "utf8": "^3.0.0" 42 | }, 43 | "devDependencies": { 44 | "prettier": "1.10.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/js/validation.js: -------------------------------------------------------------------------------- 1 | function xiamiValidation() { 2 | /** 3 | * validator for url from user 4 | */ 5 | 6 | var url = document.getElementById("xiamiUrl").value; 7 | var reg = /^https:\/\/(www)?(emumo)?.xiami.com\/space\/lib-song\/u\/\d+\/page\/2(\S+)?$/g; 8 | if (url.match(reg) == null) { 9 | alert("Invalid url, it has to be the url of second page of your playlist"); 10 | return false; 11 | } 12 | return true; 13 | } 14 | 15 | function neteaseValidation() { 16 | 17 | 18 | var url = document.getElementById("neteaseUrl").value; 19 | var reg = /^(https?|chrome):\/\/music.163.com[^\s$.?#].[^\s]*$/gm; 20 | 21 | 22 | if (url.match(reg) == null) { 23 | alert("Invalid url, Please check the url"); 24 | return false; 25 | }; 26 | return true; 27 | } 28 | 29 | function test() { 30 | var url = "https://www.xiami.com/space/lib-song/u/32935150/page/2?spm=a1z1s.6928797.1561534521.347.5oNhml" 31 | var reg = /https:\/\/(www)?(emumo)?.xiami.com\/space\/lib-song\/u\/\d+\/page\/\d\?spm=\S+$/g; 32 | console.log(url.match(reg) != null) 33 | var url = "https://emumo.xiami.com/space/lib-song/u/34340923/page/2?spm=a1z1s.6928797.1561534521.329.SCuFko"; 34 | console.log(url.match(reg) != null) 35 | 36 | 37 | url = "https://music.163.com/#/playlist?id=501341874" 38 | reg = /^(https?|chrome):\/\/music.163.com[^\s$.?#].[^\s]*$/gm 39 | console.log(url.match(reg) != null) 40 | 41 | url = "https://music.163.com/#/playlist?id=37560357&userid=43051609" 42 | console.log(url.match(reg) != null) 43 | 44 | url = "https://music.163.com/#/playlist?id=37560357&userid=43051609&sdf" 45 | console.log(url.match(reg) != null) 46 | 47 | url = "asd" 48 | console.log(url.match(reg) == null) 49 | } 50 | 51 | test() -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | const Credentials = require("./pw"); 2 | const { 3 | Client 4 | } = require('pg'); 5 | 6 | var XIAMI_TABLENAME = "xiami" 7 | var NETEASE_TABLENAME = "netease" 8 | 9 | // heroku pg:psql -a still-brushlands-47642 10 | 11 | async function connect() { 12 | // config is in local or server env 13 | credential = Credentials.DB_CONNECTION_SERVER; 14 | 15 | if (credential.connectionString === undefined) { 16 | credential = Credentials.DB_CONNECTION_LOCAL 17 | } 18 | console.log("DATABASE_URL", process.env.DATABASE_URL) 19 | const client = new Client(credential); 20 | 21 | 22 | return new Promise((resolve, reject) => { 23 | client.connect((err, client, done) => { 24 | if (err) { 25 | resolve({ 26 | success: false, 27 | message: err 28 | }) 29 | } 30 | 31 | resolve({ 32 | success: true, 33 | val: client 34 | }); 35 | }); 36 | }) 37 | } 38 | 39 | 40 | function insert(source, playListUrl) { 41 | 42 | const now = new Date() 43 | connect().then(res => { 44 | if (res.success) { 45 | var client = res.val; 46 | 47 | q = `insert into ${source} values('${playListUrl}','${now.toISOString()}');` 48 | console.log(q); 49 | client.query(q, (err, res) => { 50 | if (err) { 51 | console.debug("database connection error", err); 52 | return; 53 | } else { 54 | client.end(); 55 | console.debug("successful insert url, close connection"); 56 | } 57 | }); 58 | } 59 | }) 60 | } 61 | 62 | module.exports = { 63 | insert, 64 | XIAMI_TABLENAME, 65 | NETEASE_TABLENAME 66 | } -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | Xiami / NetEaseCLoudMusic to Spotify 17 | 18 | 19 | 20 | 21 |

22 | Xiami user: Please use this line 23 |

24 |
25 | 26 |

27 | Please go to old version after you login 28 | Xiami(请回到旧版,登陆后上面的menu就有这个选择,然后在选择我的音乐,的第二页的链接) 29 |

30 | 31 |
32 | 34 | 35 | 36 |
37 |
38 |
39 |

40 | for example: 41 |

42 | 43 |

44 | 45 | https://www.xiami.com/space/lib-song/u/18313828/page/2?spm=a1z1s.6928797.1561534521.342.HmvvYd 46 | 47 |

48 | 49 |
50 |
51 |

NetEaseCloudMusic user , please use this one,go to the playlist you want to transfer, copy the url 52 |

53 |
54 |
55 | 57 | 58 | 59 |
60 |
61 |
62 |

63 | for example: 64 |

65 | 66 |

67 | https://music.163.com/#/playlist?id=123456 68 | 69 |

70 | 71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xiami & Netease update their frontend and parser is not working. Close this project. 2 | 3 | # Music Porter 4 | 5 | ### From Xiami/NetEaseCloudMusic to Spotify implemented with Node.js 6 | 7 | ![](https://upload-images.jianshu.io/upload_images/4457561-ef588ad60e2dae00.png) 8 | 9 | If you have trouble migrate music from Xiami/NetEaseCloudMusic to Spotify, 10 | this is the application you looking for!! 11 | 12 | [Start Use Xiami/NetEaseCloudMusic to Spotify by click this link](https://still-brushlands-47642.herokuapp.com/) 13 | 14 | ### Introduction 15 | 16 | --- 17 | 18 | [Spotify access](https://developer.spotify.com/web-api/authorization-guide/) -> [xiami login](http://www.xiami.com/) -> add song page by page into Spotify 19 | 20 | [Spotify access](https://developer.spotify.com/web-api/authorization-guide/) -> [NetEase playListUrl]() -> add all songs into playlist at once 21 | 22 | [Video tutorial](https://youtu.be/gtFL4aW6IWc) 23 | 24 | ### Requirement 25 | 26 | --- 27 | 28 | Spotify Username & Password 29 | **_for Xiami_** 30 | Url of second page of your xiami PlayList 31 | 32 | **_for NetEaseCloudMusic_** 33 | playlist url 34 | 35 | **_Note : if you got error "请输入验证码", please wait for an hour or so until validatoin end for you account_** 36 | 37 | ### Rate 38 | 39 | --- 40 | 41 | Use my own data as reference, I am able to transfer 657/1300 from Xiami to Spotify. 42 | All the songs will be added into a folder named "tmp", you can change it AFTER process finish. 43 | **_Warning: if you change name during the process it mind break the program_** 44 | 45 | For **_NetEaseCloudMusic_**, it will not show the realtime update due to design issue. 46 | It will show songs in spotify all at once, which means you have to wait approximatly (1 second \* total song) until it shows 47 | 48 | ### Result 49 | 50 | --- 51 | 52 | one the web page, user will receive some ugly stirng, which has two catgory 53 | 54 | 1. passed [spotify_track_uri] | this simply means spotify did find the track 55 | 2. failed [message/error problem] | this means spotify is not able to find given track, but if you get "error" in the failed, it means somethign goes wrong with applicatoin, it is 5XX, then it is Spotiify problem, but it is 4XX, please make a issue about this. 56 | 57 | On the spotify side, the way to check this tranformation dynamically, use desktop version of Spotify. phone's spotify does not show real tiem result of update, but destop version will. 58 | 59 | ### Reference 60 | 61 | --- 62 | 63 | [Spotify API](https://developer.spotify.com/web-api/) 64 | 65 | [Xiami access](https://github.com/ovo4096/node-xiami-api/blob/master/src/crawler.js) 66 | 67 | [Node.js](https://nodejs.org/en/) 68 | 69 | ### Other 70 | 71 | feel free to extend the application, indeed ,I would happy if someone can make some css design for the page. 72 | You can make some improvement on (improve)search etc.. 73 | -------------------------------------------------------------------------------- /python_versoin/README.md: -------------------------------------------------------------------------------- 1 | # Xiami_To_Spotify 2 | 3 | ## This App no longer works due Spotify API changed, XIAMI fetch still working though 4 | ## introduction: 5 | This is a project in purpose to enable user to transfer music from [Xiami](http://www.xiami.com) to [Spotify](https://www.spotify.com/us/). 6 | 7 | The music get from **Xiami -> my music 我的音乐 -> Spotify -> playlist -> From_Xiami (we create for you)** 8 | 9 | ## Step: 10 | 11 | #### Xiami: 12 | Knowing your 13 | - [ ] username 14 | - [ ] password 15 | 16 | #### Spotify: 17 | - [ ] username (if your username if from facebook, or have space between, this app may not work for you, change it to one word) 18 | 19 | 20 | - [ ] [Spotify Application Sign in with your spotify account](https://developer.spotify.com/my-applications/) 21 | ![image](https://github.com/zhang435/Xiami_To_Spotify/blob/master/images/cretae_an_app.png) 22 | - [ ] Create Application (name whatever you want, I personally recommand 'Xiami_connection' for example) 23 | ![image](https://github.com/zhang435/Xiami_To_Spotify/blob/master/images/create_an_app_Detail.png) 24 | - [ ] after you successfully create an application, you will see Client_id and Client_Secret, keep these two information, `Us your owns` 25 | ![image](https://github.com/zhang435/Xiami_To_Spotify/blob/master/images/app_page.png) 26 | ![image](https://github.com/zhang435/Xiami_To_Spotify/blob/master/images/get_id_sec.png) 27 | - [ ] You will also see Redirect URIs after it, paste [http://github.com/zhang435/Xiami_To_Spotify/](http://github.com/zhang435/Xiami_To_Spotify/) and click add and save 28 | - [ ]Now download the [code](https://github.com/zhang435/Xiami_To_Spotify/archive/master.zip). 29 | - [ ] At the bottom at main.py, replace information with you own information. 30 | ```python 31 | if __name__ == '__main__': 32 | move = Xiami_to_Spotify('XIAMI_USERNAME','XIAMI_PASSWORD','SPOTIFY_USERNAME','CLIENT_ID','CLIENT_CECRET') 33 | move.start() 34 | ``` 35 | - [ ] go to terminal (if you use window,make sure you install python2 first [install](http://stackoverflow.com/questions/21372637/installing-python-2-7-on-windows-8), while if you are mac user, lucky you , python2 come with mac) 36 | - [ ] in terminal(in window, it is command line?)run 37 | `pip install request` 38 | `pip install spotipy` 39 | 40 | - [ ] Finally, in terminal run `python main.py`, if everything set up correctly, you should get a brower pop up on your computer, copy the link and paste it into terminal, then, just wait 41 | 42 | 43 | 44 | ## Reference: 45 | [Spotify API](https://developer.spotify.com/web-api/) 46 | 47 | [Spotipy Python Package](https://github.com/plamere/spotipy) 48 | 49 | [spotipy Documentation](http://spotipy.readthedocs.io/en/latest/) 50 | 51 | [urllib using](https://github.com/liyuntao/SignXiami) 52 | 53 | 54 | ## PROBLEM: 55 | xiami is poplar used in Chinese market, it is not surprise that there are many some that user have is in chinese name 56 | 57 | > Spotify does not have that a few Chinese song, but there most of them can be found "manually" 58 | 59 | > I blame this problem on the developer who collect song resouces, for example it has name for both Simplifed Chinese and traditional Chinese. but if you search with only simplfied , you may not find the song even it is actully there, same for traditional chinese. AND, some chinese singer are named in english....which is impossibel to search correctly 60 | 61 | > for me 900 are enable to pass 550, still need improve :) 62 | -------------------------------------------------------------------------------- /python_versoin/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import spotipy 3 | import pprint 4 | import spotipy.util as util 5 | from Login_Fetch_Xiami import * 6 | from Search_Artists_Song_Spotify import * 7 | 8 | web ='http://github.com/zhang435/Xiami_To_Spotify/' 9 | scope = 'playlist-modify' 10 | 11 | class Xiami_to_Spotify(object): 12 | def __init__(self,xiami_email,xiami_password,username,iD,sec, web = web): 13 | self.iD = iD 14 | self.sec= sec 15 | self.web =web 16 | 17 | self.xiami_email = xiami_email 18 | self.xiami_password = xiami_password 19 | self.username = username 20 | self.token = util.prompt_for_user_token( 21 | client_id=iD, client_secret=sec, redirect_uri=web, username=username, scope=scope) 22 | 23 | def start(self): 24 | user= LoginXiami(self.xiami_email,self.xiami_password) 25 | user.login() 26 | while not self.token: 27 | print("\033[91mWrong information, Please change it and call function again\033[0m") 28 | sys.exit() 29 | sp = spotipy.Spotify(auth=self.token) 30 | 31 | songs = user.fetch_faviort() 32 | songs , total = songs[0],songs[1] 33 | results = [] 34 | error = [] 35 | 36 | 37 | # add the music into defualt playlsit 38 | # we call From_Xiami 39 | playlists = sp.user_playlists(self.username) 40 | tag = True 41 | temp = None 42 | for playlist in playlists['items']: 43 | if "From_Xiami" == playlist['name']: 44 | tag = False 45 | temp = playlist['id'] 46 | break 47 | 48 | if tag: 49 | print("\x1b[1;32mCreate 'From_Xiami' in your playlist\x1b[0m") 50 | print(self.username) 51 | sp.user_playlist_create(self.username, "From_Xiami") 52 | playlists = sp.user_playlists(self.username) 53 | for playlist in playlists['items']: 54 | if "From_Xiami" == playlist['name']: 55 | temp = playlist['id'] 56 | break 57 | # print(playlist['id'], playlist['name']) 58 | # print(temp,songs) 59 | print("\x1b[1;32mStart add songs into Spotify default playlist\x1b[0m") 60 | count = 0 61 | for song in songs: 62 | # sp = spotipy.Spotify(auth=self.token) 63 | count+=1 64 | sys.stdout.write("\r") 65 | sys.stdout.write("\x1b[1;36m... In process ..."+str(count)+"/"+str(total)+"\x1b[0m") 66 | sys.stdout.flush() 67 | 68 | data = Search_Singers_Song(song[1], song[0]) 69 | song = data.get_song_uri() 70 | # the return type of Search_Singers_Song 71 | # (boolan ,song,singer) 72 | # if we did not find the right inforamtion, we still need to record it, to let user know 73 | if song[0]: 74 | results.append(song[2]) 75 | else: 76 | error.append(song) 77 | if count%99 == 0: 78 | self.token = util.prompt_for_user_token( 79 | client_id=self.iD, client_secret=self.sec, redirect_uri=self.web, username=self.username, scope=scope) 80 | if results: 81 | sp.user_playlist_add_tracks(self.username, temp, results) 82 | results = [] 83 | 84 | print("\033[94m...Waiting...\033[0m") 85 | if results: 86 | results = sp.user_playlist_add_tracks(self.username, temp, results) 87 | print('\x1b[6;20;43m Success!\x1b[0m') 88 | 89 | 90 | print("\033[91mbelow's some are not avaliable in the market, or it using differnt name in Spotify") 91 | 92 | print("*" * 30+"*" * 30+"\n") 93 | print(str(len(error))+ " in total\033[0m") 94 | for i in error: 95 | print i[1] + " : " + i[2] 96 | print("\033[91m"+"*" * 30+"*" * 30+"\n"+"\033[0m") 97 | print('\x1b[6;20;43m '+str(error+0.00001)/total+'% successfully added\x1b[0m') 98 | 99 | if __name__ == '__main__': 100 | move = Xiami_to_Spotify('Xiami_Username','Xiami_password','Spotify_useranem',client_id,client_scert) 101 | move.start() 102 | -------------------------------------------------------------------------------- /NetEaseCloudMusic.js: -------------------------------------------------------------------------------- 1 | const cheerio = require("cheerio") 2 | const suepragent = require("superagent") 3 | 4 | const NETEASEMUSICURL = "https://music.163.com" 5 | const testconst = require("./test"); 6 | 7 | module.exports = { 8 | generateSongSingers 9 | } 10 | 11 | 12 | /** 13 | * use playlist url to get the 14 | * @param {String} link the playlist link, it will fetch table html content to get_song_profile 15 | */ 16 | function fetch_page(link) { 17 | return new Promise((resolve, reject) => { 18 | suepragent 19 | .get(link) 20 | .end((err, res) => { 21 | if (err) 22 | reject({ 23 | "WTF": "!!!!!!", 24 | err 25 | }); 26 | 27 | $ = cheerio.load(res.text); 28 | resolve($('.f-hide').html()); 29 | }); 30 | }) 31 | } 32 | 33 | /** 34 | * in neteaseCloudMusic, there is a profile url for each individual song, the playlist site will 35 | * contains the url for each song, where I use the profile to get track & singer data 36 | * TODO: the reason I choice this way is that, even though we get the playlist page, we 37 | * can not get the singer content for some reason, the html content does not include the 38 | * singer but only the song name, one thing I noticed is that it has loading words in the 39 | * html content, which means when the page load, it has partial content that is 40 | * not fully loaded from the back end, so it is the reason why it's not showing the full content 41 | * let me know if you actually know the solution time fix this problem 42 | * I have limit of time to spend on this project at this point, a PR is better 43 | * @param {string} link link to the profile page 44 | */ 45 | async function get_song_profile(link) { 46 | console.log("this is the url", link); 47 | var content = await fetch_page(link).catch(error => error); 48 | return new Promise((resolve, rej) => { 49 | 50 | var profiles = Array(); 51 | $ = cheerio.load(content); 52 | $("a").each((index, element) => { 53 | profiles.push($(element).attr("href")); 54 | }) 55 | resolve(profiles); 56 | rej("error in get_song_profile"); 57 | }) 58 | } 59 | 60 | 61 | /** 62 | * get song singer for one song 63 | * @param {*} link 64 | */ 65 | 66 | async function getSongSingerFromProfilePage(link) { 67 | return new Promise((resolve, reject) => { 68 | suepragent 69 | .get(NETEASEMUSICURL + link) 70 | .end((err, res) => { 71 | if (err) { 72 | reject(err); 73 | } 74 | $ = cheerio.load(res.text); 75 | var song = $("em.f-ff2").text(); 76 | var singer = $("span").children(".s-fc7").text(); 77 | resolve(new Array(song, singer)); 78 | reject("error in getSongSingerFromProfilePage"); 79 | }) 80 | }) 81 | } 82 | 83 | /** 84 | * get song singer for each song 85 | * this should be the only entrance for the netEaseMusic 86 | * @param {*} link to thr playlist 87 | */ 88 | async function generateSongSingers(link, sp, res) { 89 | link = link.replace("/#/", "/"); 90 | var profiles = await get_song_profile(link).catch(error => res.write(JSON.stringify(error) + "1")); 91 | var songArtists = new Array(); 92 | for (var i = 0; i < profiles.length; i++) { 93 | var song_singer = await getSongSingerFromProfilePage(profiles[i]).catch(error => res.write(JSON.stringify(error) + "2")); 94 | console.log(song_singer); 95 | songArtists.push(song_singer); 96 | if (songArtists.length == 20 || (i == profiles.length - 1)) { 97 | var uris = await sp.getSongsURI(songArtists).catch(error => res.write(JSON.stringify(error) + "3")); 98 | if (uris.success) { 99 | res.write(JSON.stringify(uris) + "
"); 100 | } else { 101 | res.write(uris.message); 102 | return; 103 | } 104 | 105 | sp.addSongsToPlaylist(uris.val.uris).then((result) => { 106 | // res.write(JSON.stringify(result) + "\n"); 107 | }).catch((err) => { 108 | res.write(JSON.stringify(err)); 109 | return; 110 | }); 111 | songArtists = new Array(); 112 | } 113 | } 114 | 115 | // res.write("

search fetched songs in Spotify

"); 116 | 117 | return new Promise((resolve, rej) => { 118 | resolve({ 119 | success: true, 120 | val: null 121 | }); 122 | rej("can not get song singers") 123 | }) 124 | 125 | 126 | } 127 | 128 | 129 | // // generateSongSingers(); 130 | // var netEaselink1 = "https://music.163.com/playlist?id=501341874"; 131 | // var netEaselink2 = "https://music.163.com/playlist?id=11879687"; 132 | // var netEaselink3 = "https://music.163.com/#/playlist?id=2542915771"; 133 | // // console.debug(testconst) 134 | // (async () => { 135 | // var songArtists = await generateSongSingers(netEaselink1, null).catch(err => console.log(err)); 136 | // console.log(songArtists); 137 | // })() -------------------------------------------------------------------------------- /python_versoin/Login_Fetch_Xiami.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''''' 3 | Created on 2017-12-29 4 | 5 | @author: Jiawei Zhang 6 | ''' 7 | import urllib 8 | import urllib2 9 | import cookielib 10 | import sys 11 | import base64 12 | import re 13 | from utf8_trans import to_utf8 14 | import spotipy 15 | 16 | 17 | class LoginXiami: 18 | login_header = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4'} 19 | signin_header = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31', 20 | 'X-Requested-With': 'XMLHttpRequest', 'Content-Length': '0', 'Origin': 'http://www.xiami.com', 'Referer': 'http://www.xiami.com/'} 21 | cookie = None 22 | cookieFile = './cookie.dat' 23 | 24 | def __init__(self, email, pwd): 25 | self.email = email 26 | self.password = (pwd) 27 | self.cookie = cookielib.LWPCookieJar() 28 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookie)) 29 | urllib2.install_opener(opener) 30 | # Login 31 | def login(self): 32 | postdata = {'email': self.email, 'password': self.password, 33 | 'done': 'http://www.xiami.com', 'submit': '%E7%99%BB+%E5%BD%95'} 34 | 35 | postdata = urllib.urlencode(postdata) 36 | # print(postdata) 37 | print('Logining...') 38 | req = urllib2.Request(url='http://www.xiami.com/member/login', data=postdata, headers=self.login_header) 39 | # read the html code from the web page 40 | result = urllib2.urlopen(req).read() 41 | # add it into cookie 42 | self.cookie.save(self.cookieFile) 43 | result = str(result) 44 | if '密码错误' in result: 45 | print('\033[91mLogin failed due to Email or Password error...\033[0m') 46 | sys.exit() 47 | return True 48 | else: 49 | print('\033[92mLogin successfully!\033[0m') 50 | return False 51 | # get the data from html 52 | # you can turn_off print statment, this may accel the speed of whole process 53 | # xxx.fetch_faviort(prin = False) 54 | 55 | def fetch_faviort(self,prin = True): 56 | postdata = {} 57 | postdata = urllib.urlencode(postdata) 58 | print('\033[94mVisiting your current Faviort\033[0m') 59 | page = 1 60 | song_artist = [] 61 | total_music = 0 62 | while 1: 63 | # go to website 64 | req = urllib2.Request(url='http://www.xiami.com/space/lib-song/page/' +str(page),data=postdata, headers=self.signin_header) 65 | 66 | content_stream = urllib2.urlopen(req) 67 | result = content_stream.read() 68 | if 'class="artist_name"' not in result: 69 | print('\x1b[1;32m'+'#' * 20 + '#' * 20) 70 | print('#' * 20 + '#' * 20+"\x1b[1;0m") 71 | 72 | print("\x1b[1;36mfinish fetching all music in Xiami\nNow start add musics into Spotify\x1b[1;0m") 73 | print str(total_music),"in total" 74 | print('\x1b[1;32m#' * 20 + '#' * 20) 75 | print('#' * 20 + '#' * 20+"\x1b[0m") 76 | return [song_artist,total_music] 77 | if prin: 78 | print('\x1b[1;36m#' * 20 + '#' * 20) 79 | print("Currently In page " + str(page)) 80 | print('#' * 20 + '#' * 20+"\x1b[1;0m") 81 | 82 | musictable = re.findall('(?<=).+?(?=)', result, re.DOTALL) 83 | total_music += len(musictable) 84 | for i in musictable: 85 | # get the information from the html base on differnt tag 86 | song_name = re.search('(?<=)', i, re.DOTALL).group(0) 92 | artist_name = re.search('(?<=">).+', artist_name, re.DOTALL).group(0) 93 | artist_name = artist_name.replace("'", "'") 94 | # improve the searching og both song name and artist name 95 | if ' (Live)' in song_name: 96 | song_name = song_name.replace(" (Live)","") 97 | sp = spotipy.Spotify() 98 | result = sp.search(q='artist:' + to_utf8(artist_name), type='artist') 99 | if result['artists']['items']: 100 | artist_name = result['artists']['items'][0]['name'] 101 | print to_utf8(song_name), 102 | print ' :', to_utf8(artist_name) 103 | song_artist.append([to_utf8(song_name), to_utf8(artist_name)]) 104 | page += 1 105 | def fetch_by_playlist(self): 106 | # http://www.xiami.com/space/collect/u/18313828 107 | # under deverlopemnt 108 | # this function is not necessary for me, but I know some people want 109 | # enable user to move music base on the playlist but not just add all of them into one playlist 110 | pass 111 | 112 | # if __name__ == '__main__': 113 | # # user = LoginXiami(Xiami_Username, Xiami_password) 114 | # user.login() 115 | # ans = user.fetch_faviort() 116 | user = LoginXiami("apple19950105@gmail.com","apple19950105") 117 | user.login() 118 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | netEaselink1, 3 | netEaselink2, 4 | netEaselink3 5 | } 6 | 7 | var netEaselink1 = "https://music.163.com/playlist?id=501341874"; 8 | var netEaselink2 = "https://music.163.com/playlist?id=11879687"; 9 | var netEaselink3 = "https://music.163.com/#/playlist?id=2542915771"; 10 | 11 | var test_data = { 12 | 'Always on My Mind': 'Elvis Presley', 13 | '兰州 兰州': '低苦艾', 14 | 'Hymn for the Weekend': 'Coldplay', 15 | '不和リン': '青叶市子', 16 | 'クロノスタシス': 'きのこ帝国', 17 | '米店': '张玮玮郭龙', 18 | '若水': '西楼', 19 | '啊朋友 再见': '蒋明冬子刘东明好妹妹乐队钟立风小河', 20 | 'Waltz by the River': 'Eleni Karaindrou', 21 | '青春': '韩红', 22 | 'The Days': 'AviciiRobbie Williams', 23 | 'The Nights': 'AviciiRas', 24 | 'Mi Gente': 'J. BalvinWilly WilliamBeyoncé', 25 | 'Bank Account': '21 Savage', 26 | '姐姐': '李雨王鼎渊', 27 | '无法长大': '赵雷', 28 | '同步': '范晓萱', 29 | '最冷一天': '张国荣', 30 | '当爱已成往事': '张国荣', 31 | '无底洞': '蔡健雅', 32 | '不搭': '李荣浩', 33 | '祝你幸福': '李荣浩', 34 | 'Let It Go (Bearson Remix)': 'BearsonJames Bay', 35 | 'Another Day In Paradise': 'Phil Collins', 36 | 'Over the Rainbow': 'MichitaKeyco', 37 | '幻期颐': '粒粒', 38 | 'Brazilian Rhyme (Almost There)': 'DJ SLYD.OKaoru', 39 | '现在的样子': '戴佩妮', 40 | 'MINMI/Nujabes - Song Of Four Seasons (Shiki No Uta)': 'DJ Vital Force', 41 | 42 | 'I (Love Myself) [Jean Blanc Remix]': 'Jean BlancKendrick LamarJackson Breit', 43 | 'Lean On': 'Major LazerMØDJ Snake', 44 | Tattoo: '小林武史', 45 | Wiggle: 'Jason DeRuloSnoop Dogg', 46 | '一念之间': '张杰莫文蔚', 47 | '阳光中的向日葵': '马条', 48 | '北方': '倪健', 49 | 'Spirited Beginning (Nujabes Tribute)': 'Thomas Prime', 50 | 'Boom (Original Mix)': 'TiëstoSevenn', 51 | 'Marilyn Monroe': 'SEVDALIZA', 52 | '双栖动物': '蔡健雅', 53 | '美丽世界的孤儿': '汪峰', 54 | '存在': '汪峰', 55 | '一起摇摆': '汪峰', 56 | '生来彷徨': '汪峰', 57 | '海芋恋': '萧敬腾', 58 | 'I´m Not The Only One': 'Sam SmithA$AP RockyJean Blanc', 59 | 'There For You': 'Martin GarrixTroye Sivan', 60 | '痒': '黄龄', 61 | '不要爱我': '薛凯琪方大同', 62 | 'How to Love': 'Cash CashSofia Reyes', 63 | '作曲家': '李荣浩', 64 | '梦田': '齐豫潘越云', 65 | '迷途羔羊': '张震岳大渊', 66 | 'Easy Love': 'Sigala', 67 | '秦皇岛': '万能青年旅店', 68 | 'Dear Friend': '顺子', 69 | '想太多': '李玖哲', 70 | '就像是一块没有记忆的石头': '陈小熊', 71 | 'Addicted to Your Love (ConKi Edit)': 'Con​KiThe Shady Brothers', 72 | '突然想起你 (Live)': '林宥嘉', 73 | '星期三或礼拜三': '魏如萱马頔', 74 | '第三人称 (Live) ': 'Hush!', 75 | '脱缰': '陈粒', 76 | 'Airplanes ': 'B.o.BHayley Williams', 77 | 'Daydreamer (Original Mix)': 'KarlKGuitK', 78 | '你就要走了': '花粥', 79 | '用情': '张信哲', 80 | '女孩儿': '不可撤销乐队', 81 | '丑': '草东没有派对', 82 | '大风吹': '草东没有派对', 83 | 'Smash The Funk': 'GRiZ', 84 | 'Summer Wine': 'Lana Del Rey', 85 | '美貌の青空': '大貫妙子坂本龍一', 86 | 'Samsara (feat. Emila) [Extended Mix]': 'Tungevaag & Raaban', 87 | 'Go Solo': 'Tom Rosenthal', 88 | 'PIANO UC-NO.3': '澤野弘之', 89 | 'Wise Man': 'Frank Ocean', 90 | Tadow: 'FKJMasego', 91 | '当年情': '张国荣', 92 | 'アルカレミア': 'mol-74', 93 | 'Wide Open': 'The Chemical BrothersBeck', 94 | 'The Invisible Girl': 'Parov Stelar Trio', 95 | 'Soul Below': 'Ljones', 96 | 'Be Mine': 'Jazzinuf', 97 | '当我想你的时候 (Live)': '汪峰', 98 | '北京北京 (Live)': '汪峰', 99 | 'Big Jet Plane': 'Angus & Julia Stone', 100 | 'I & I': 'Leola', 101 | 'Winter Reflection (Nujabes Tribute)': 'Niazura', 102 | 'Wildcard (Extended Mix)': 'KSHMR', 103 | 'Stay Gold': '大橋トリオ', 104 | Oceano: 'Roberto Cacciapaglia', 105 | '历历万乡': '陈粒', 106 | 'いい日旅立ち': '山口百恵', 107 | 'いい日旅立ち ': '谷村新司', 108 | 'Ame no akogare (Ode to Nujabes)': 'Romo', 109 | Thunder: 'Imagine Dragons', 110 | '张三的歌(Cover 张悬)': '「么凹」', 111 | '孩子': '西楼', 112 | '作曲家': '李荣浩', 113 | '不说': '李荣浩', 114 | '小芳': '李春波', 115 | 'ウヲアイニ': '岩井俊二', 116 | Flow: '方大同王力宏', 117 | '小方': '方大同', 118 | '夜已如歌': '绿色频道', 119 | '你快乐(所以我快乐)': '王菲', 120 | 'スパークル (movie ver.)': 'RADWIMPS', 121 | '神秘嘉宾': '林宥嘉', 122 | Free: 'Plan B', 123 | 'Wonderful Tonight (Live)': 'Eric Clapton', 124 | '人海': '燕池', 125 | 'Secrets (Original Mix)': 'TiëstoKSHMRVassy', 126 | Transmigration: '邱比', 127 | 'Can’t Sleep Love': 'Pentatonix', 128 | 'Am I Wrong': 'Nico & Vinz', 129 | 'Get Lucky': 'Daft PunkPharrell Williams', 130 | '纪念': '蔡健雅', 131 | 'Happy End': '坂本龍一', 132 | 'Spartacus Love Theme': 'Re:plus', 133 | '想把我唱给你听': '老狼王婧', 134 | '乘客': '王菲', 135 | 'Heaven Sent': 'J-LouisZacari', 136 | 'Stereo Hearts ': 'Gym Class HeroesAdam Levine' 137 | } 138 | test_data = Object.keys(test_data).map(x => [x, test_data[x]]) 139 | 140 | var access_token = "BQBWdMgpWXsqkZvHLkvxf_RSpWNdmKgAvw9sZ6-LwIR6Nb54bVlellgo3VeMfvbXFvbN1Mi6jH6MBMbtE6xxhgPI7yCMNT__bi51xpDsJobahO1zIN4-VKF4_Bxhep2uQ_7i6ARKPX0LlvEU2EKthcLQ0J9BMs1NVyUPqSova2khY3O9bDA_rB9teTC4ujFsKeb-JRugJ2Yaz9U"; 141 | 142 | 143 | 144 | // console.log(test_data.length); 145 | 146 | // get_songs_uri(test_data, access_token); 147 | 148 | 149 | 150 | // async function test() { 151 | // await create_playlist("zhang435", access_token); 152 | // var username = await get_user_id(access_token).catch(error => console.log(error)); 153 | 154 | // if (!username){ 155 | // print("error during get username") 156 | // return; 157 | // } 158 | // print(username); 159 | // var playlist = await get_playlist_id(username, access_token).catch(error => console.log(error)); 160 | // if (!playlist){ 161 | // print("error during get playlist") 162 | // return 163 | // } 164 | 165 | // print(playlist); 166 | // var arr = await get_songs_uri(test_data, access_token).catch(error => console.log(error)); 167 | 168 | // if (!arr){ 169 | // print("error during get song uri") 170 | // return; 171 | // } 172 | 173 | // console.log(arr.passed); 174 | // add("zhang435", playlist, arr.passed, access_token).catch(error => console.log(error)); 175 | // } 176 | 177 | // test(); -------------------------------------------------------------------------------- /Xiami.js: -------------------------------------------------------------------------------- 1 | const cheerio = require("cheerio"); 2 | const suepragent = require("superagent"); 3 | const rp = require("request-promise"); 4 | const _ = require("lodash"); 5 | 6 | /** 7 | * Xiami.js mainly handle the job for user to acess the xiami account and featch the playlist contents 8 | */ 9 | 10 | module.exports = { 11 | init 12 | }; 13 | 14 | function Xiami(playlistUrl) { 15 | this.xiamiToken; 16 | this.userID; 17 | this.spmCode; 18 | /** 19 | * 20 | * @param {*} url http url for the user playlist 21 | */ 22 | 23 | this.extractUrl = async url => { 24 | /** 25 | * extract userID,page_num,spmCode from url 26 | * @param url String : xiami playlist url 27 | * @returns {userID String, spmCode String} 28 | */ 29 | var contents = _.split(url, "/"); 30 | return new Promise((resolve, reject) => { 31 | resolve({ 32 | success: true, 33 | val: { 34 | userID: contents[6], 35 | spmCode: _.split(contents[8], "spm=")[1] 36 | } 37 | }); 38 | }); 39 | }; 40 | 41 | /** 42 | * _include_headers: this method will able rp to return whole http response instead of just body content 43 | * @param {*} html content from request 44 | * @param {*} response http response 45 | * @param {*} resolveWithFullResponse None 46 | */ 47 | function _include_headers(body, response, resolveWithFullResponse) { 48 | return { 49 | headers: response.headers, 50 | data: body 51 | }; 52 | } 53 | 54 | /** 55 | * fetchXiamiToken: send a request to the login page, which will send back a xiami access token for login 56 | * @returns Promise 57 | */ 58 | this.fetchXiamiToken = async () => { 59 | var options = { 60 | method: "GET", 61 | uri: "https://login.xiami.com/member/login", 62 | transform: _include_headers 63 | }; 64 | 65 | return new Promise((resolve, reject) => { 66 | rp(options) 67 | .then(response => { 68 | // console.log(response.headers['set-cookie'], "!!!") 69 | resolve({ 70 | success: true, 71 | val: response.headers["set-cookie"][2].match( 72 | /_xiamitoken=(\w+);/ 73 | )[1] 74 | }); 75 | }) 76 | .catch(error => { 77 | reject({ 78 | success: false, 79 | message: "unable to access the page : " + options.uri 80 | }); 81 | }); 82 | }); 83 | }; 84 | 85 | /** 86 | * feach the coeresponding playlist page of user 87 | * @param {dict} user_info 88 | * @param {int} page_num the page num of my playlist 89 | * @param {String} xiamiToken get access token for the user 90 | */ 91 | 92 | this.fetchPage = async (page_num) => { 93 | var standard_url = `http://www.xiami.com/space/lib-song/u/${ 94 | this.userID 95 | }/page/${page_num}`; 96 | // console.log(standard_url); 97 | return new Promise((resolve, reject) => { 98 | suepragent 99 | .get(standard_url) 100 | .set("Cookie", `_xiamitoken=${this.xiamiToken}`) 101 | .end((error, res) => { 102 | if (error) { 103 | reject({ 104 | success: false, 105 | message: "unable to load page " + standard_url 106 | }); 107 | } else { 108 | resolve({ 109 | success: true, 110 | val: res 111 | }); 112 | } 113 | }); 114 | }); 115 | } 116 | 117 | /** 118 | * extract the sgong singer content from paylist table base on the html content send from the fetchPage 119 | * @param {html String} content html page 120 | * @returns {String, String} {song, singer} 121 | */ 122 | this.generateSongSinger = async function (content) { 123 | /** 124 | * generate_song_singer : iteriate the whole userplaylist to get the data from Xiami 125 | * Stirng -> {song @String : singer @String } 126 | */ 127 | var song_singers = Array(); 128 | $ = cheerio.load(content.text); 129 | $(".song_name").each((i, element) => { 130 | var song = $(element) 131 | .find("a") 132 | .first() 133 | .text(); 134 | var singer = $(element) 135 | .find(".artist_name") 136 | .text(); 137 | song_singers.push([song, singer]); 138 | }); 139 | return new Promise((resolve, reject) => { 140 | resolve({ 141 | "success": true, 142 | "val": song_singers 143 | }) 144 | }); 145 | }; 146 | } 147 | 148 | async function init(url) { 149 | var xm = new Xiami(url); 150 | var _ = await xm.fetchXiamiToken().catch(err => err); 151 | 152 | if (_.success) { 153 | xm.xiamiToken = _.val; 154 | } else { 155 | return new Promise((resolve, reject) => { 156 | reject({ 157 | success: false, 158 | message: _ 159 | }); 160 | }); 161 | } 162 | 163 | var _ = await xm.extractUrl(url).then(err => err); 164 | if (_.success) { 165 | xm.userID = _.val.userID; 166 | xm.spmCode = _.val.spmCode; 167 | } else { 168 | return new Promise((resolve, reject) => { 169 | reject({ 170 | success: false, 171 | message: _ 172 | }); 173 | }); 174 | } 175 | 176 | return new Promise((resolve, reject) => { 177 | resolve({ 178 | success: true, 179 | val: xm 180 | }); 181 | }); 182 | }; 183 | 184 | // var url = 185 | // "https://www.xiami.com/space/lib-song/u/18313828/page/2?spm=a1z1s.6928797.1561534521.379.Vf6tln"; 186 | // var x = init(url) 187 | // .then(res => { 188 | // res.val.fetchPage(40).then(x => { 189 | 190 | // }).catch(err => { 191 | // console.log(err); 192 | // }) 193 | // }) 194 | // .catch(); 195 | 196 | // (async () => { 197 | // var xm = await init(url).catch(err => err); 198 | // if (!xm.success) { 199 | // console.log(xm.err); 200 | // return; 201 | // } else { 202 | // xm = xm.val; 203 | 204 | // } 205 | 206 | // var page = await xm.fetchPage(20).catch(err => err); 207 | // if (!page.success) { 208 | // console.log(page.message); 209 | // return; 210 | // } else { 211 | // page = page.val; 212 | // } 213 | 214 | // // console.log(page); 215 | 216 | // var song_singers = await xm.generateSongSinger(page); 217 | // console.log(song_singers.val); 218 | // })(); -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); // Express web server framework 2 | const request = require('request'); // "Request" library 3 | const querystring = require('querystring'); 4 | const hbs = require("hbs"); 5 | const app = express(); 6 | 7 | const port = process.env.PORT || 8888; 8 | 9 | const Spotify = require("./Spotify"); 10 | const Xiami = require("./Xiami"); 11 | const NetEase = require("./NetEaseCloudMusic"); 12 | const db = require("./db"); 13 | // const account = require("./account") 14 | 15 | 16 | var bodyParser = require('body-parser') 17 | app.use(bodyParser.json()); // to support JSON-encoded bodies 18 | app.use(bodyParser.urlencoded({ // to support URL-encoded bodies 19 | extended: true 20 | })); 21 | 22 | app.use(express.static('src')) 23 | 24 | const authorizeUrl = 'https://accounts.spotify.com/authorize?'; 25 | 26 | //////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | /** 28 | * request the access token for spotify 29 | */ 30 | app.get("/", (req, res) => { 31 | /** 32 | * @param {} when user first get into the page, go thorugh the auth process 33 | * @return {promise} 34 | */ 35 | 36 | var QUERY_PARAMETER = { 37 | client_id: Spotify.CLIENT_ID, 38 | response_type: "code", 39 | redirect_uri: Spotify.REDIRECT_URI, 40 | scope: Spotify.SCOPE 41 | } 42 | res.redirect(authorizeUrl + querystring.stringify(QUERY_PARAMETER)) 43 | }) 44 | 45 | /** 46 | * redirect back to the homepage with access token 47 | */ 48 | app.get("/callback", (req, res) => { 49 | var code = req.query.code || null; 50 | 51 | if (code == null) 52 | res.end("Missing code from Spotify redirect"); 53 | 54 | // requests_refresh_and_access_tokens 55 | REQUEST_BODY_PARAMETER = { 56 | url: 'https://accounts.spotify.com/api/token', 57 | form: { 58 | grant_type: "authorization_code", 59 | code: code, 60 | redirect_uri: Spotify.REDIRECT_URI 61 | }, 62 | headers: { 63 | 'Authorization': 'Basic ' + (new Buffer(Spotify.CLIENT_ID + ':' + Spotify.CLIENT_SECRET).toString('base64')) 64 | }, 65 | json: true 66 | } 67 | 68 | 69 | 70 | request.post(REQUEST_BODY_PARAMETER, (error, response, body) => { 71 | console.log(body); 72 | res.render("index.hbs", { 73 | accessToken: body.access_token 74 | }) 75 | }); 76 | }) 77 | 78 | // handler for Xiami 79 | app.post("/xiami", (req, res) => { 80 | var playlistUrl = req.body.playlistUrl; 81 | var spotifyAccessToken = req.body.spotifyAccessToken; 82 | if ([playlistUrl, spotifyAccessToken].includes(undefined)) { 83 | res.end("got undefined value when rendering Xiami", JSON.stringify([playlistUrl, spotifyAccessToken])); 84 | } 85 | 86 | res.setHeader("Content-Type", "text/html; charset=utf-8"); 87 | 88 | // record the user playlist url for debugging purpose 89 | db.insert(db.XIAMI_TABLENAME, playlistUrl); 90 | res.write("

start import Xiami playlist to spotify

"); 91 | xiamiProcess(playlistUrl, spotifyAccessToken, res); 92 | }) 93 | 94 | // handler for NetEaseMusic 95 | app.post("/NetEaseCloudMusic", (req, res) => { 96 | var spotifyAccessToken = req.body.spotifyAccessToken; 97 | const NetEaseCloudMusicUrl = req.body.playlistUrl; 98 | res.setHeader("Content-Type", "text/html; charset=utf-8"); 99 | 100 | // record the user playlist url for debugging purpose 101 | db.insert(db.NETEASE_TABLENAME, NetEaseCloudMusicUrl); 102 | res.write("

start import netease playlist to spotify " + NetEaseCloudMusicUrl + "

"); 103 | NetEaseProcess(spotifyAccessToken, NetEaseCloudMusicUrl, res); 104 | }) 105 | 106 | 107 | 108 | async function xiamiProcess(url, spotifyAccessToken, res) { 109 | var xm = await Xiami.init(url).catch(err => err); 110 | var sp = await Spotify.init(spotifyAccessToken, "Xiami ").catch(err => err); 111 | 112 | if (!xm.success || !sp.success) { 113 | res.end(JSON.stringify({ 114 | "xiami": xm.message, 115 | "spotify": sp.message 116 | })); 117 | return; 118 | } else { 119 | xm = xm.val; 120 | sp = sp.val; 121 | } 122 | 123 | for (var i = 1;; i++) { 124 | 125 | var page = await xm.fetchPage(i).catch(err => err); 126 | if (!page.success) { 127 | res.end(page.message); 128 | return; 129 | } else { 130 | page = page.val; 131 | } 132 | console.debug("page", i); 133 | // console.debug(page); 134 | 135 | var xmSongSingers = await xm.generateSongSinger(page).catch(err => err); 136 | if (!xmSongSingers.success) { 137 | res.end(xmSongSingers.message); 138 | return; 139 | } else { 140 | xmSongSingers = xmSongSingers.val; 141 | } 142 | 143 | if (xmSongSingers.length == 0) { 144 | break; 145 | } 146 | 147 | var uris = await sp.getSongsURI(xmSongSingers); 148 | if (uris.success) { 149 | res.write(JSON.stringify(uris)); 150 | } else { 151 | res.end(uris.message); 152 | return 153 | } 154 | 155 | 156 | console.debug("reaching the end of page", i); 157 | sp.addSongsToPlaylist(uris.val.uris).then((result) => { 158 | res.write(JSON.stringify(result)); 159 | }).catch((err) => { 160 | res.end(JSON.stringify(err.message)); 161 | return 162 | }); 163 | 164 | }; 165 | res.end("

Finished

"); 166 | } 167 | 168 | async function NetEaseProcess(spotifyAccessToken, NetEaseCloudMusicUrl, res) { 169 | 170 | var sp = await Spotify.init(spotifyAccessToken, "NetEase ").catch(err => err); 171 | if (!sp.success) { 172 | res.end(JSON.stringify({ 173 | "spotify": sp.message 174 | })); 175 | return; 176 | } else { 177 | sp = sp.val; 178 | } 179 | 180 | var songArtists = await NetEase.generateSongSingers(NetEaseCloudMusicUrl, sp, res).catch(err => res.write(JSON.stringify(err))); 181 | console.debug("got all songs from NetEase"); 182 | res.end("

done,check 'from NetEase' in Spotify

"); 183 | } 184 | 185 | app.listen(port); -------------------------------------------------------------------------------- /Spotify.js: -------------------------------------------------------------------------------- 1 | const rp = require("request-promise"); 2 | 3 | const CLIENT_ID = "c778e8173793481c907f2ee677fdf578"; // Your client id 4 | const CLIENT_SECRET = "3d5d8daa997a4b29b11100d55b018ad2"; // Your secret 5 | const url = "https://still-brushlands-47642.herokuapp.com/" 6 | // const url = "http://localhost:8888/"; 7 | 8 | const REDIRECT_URI = url + "callback"; // Your redirect uri 9 | const SCOPE = 10 | "playlist-modify-public playlist-read-collaborative playlist-modify-private"; 11 | 12 | module.exports = { 13 | CLIENT_ID, 14 | CLIENT_SECRET, 15 | REDIRECT_URI, 16 | SCOPE, 17 | init 18 | }; 19 | 20 | /** 21 | * Spotify wrapper object 22 | * 23 | * @param {*} accessToken xiami access token 24 | * @param {*} source playlistName, if the user start from Xiami, source = xiami, or if user come from netEase, the source = NetEase 25 | * **/ 26 | 27 | function Spotify(accessToken, source) { 28 | this.accessToken = accessToken; 29 | this.playListName = "From " + source; 30 | this.userID = null; 31 | this.playListID = null; 32 | 33 | /** 34 | * get_user_id : return the user id for the user, some user created with facebook may encounter actual useranme does not match with the one they know 35 | * String -> Promise 36 | * => Promise with user's id 37 | */ 38 | this.getUserID = async () => { 39 | var options = { 40 | url: "https://api.spotify.com/v1/me", 41 | headers: { 42 | Authorization: "Bearer " + this.accessToken 43 | }, 44 | json: true 45 | }; 46 | 47 | console.debug(this.accessToken, this.playListName); 48 | 49 | return new Promise((resolve, reject) => { 50 | rp(options) 51 | .then(body => { 52 | // get the user id through API 53 | if (body.id) { 54 | return resolve({ 55 | success: true, 56 | val: body.id 57 | }); 58 | } 59 | 60 | return reject({ 61 | success: false, 62 | message: "Unable to get data for" + this.accessToken 63 | }); 64 | }) 65 | .catch(error => { 66 | return reject({ 67 | success: false, 68 | message: "error when sending the request to getUserID, error : " + 69 | error + this.accessToken + "?" 70 | }); 71 | }); 72 | }); 73 | }; 74 | 75 | /** 76 | * check_playlist : all the song fetch from xiami will been store in a playlist, which name as tmp, this is just prevent dupliate creation 77 | * @returns Promise 78 | */ 79 | this.ifPlayListExists = async () => { 80 | var options = { 81 | url: "https://api.spotify.com/v1/users/" + this.userID + "/playlists", 82 | headers: { 83 | Authorization: "Bearer " + this.accessToken 84 | }, 85 | json: true 86 | }; 87 | 88 | return new Promise((resolve, reject) => { 89 | // if this function been used before userID/accessToekn defined, rej 90 | if (this.userID === undefined || this.accessToken === undefined) { 91 | return reject({ 92 | success: false, 93 | message: "userID/accessToken is not initlized yet, code error" 94 | }); 95 | } 96 | 97 | // go through all playlist, make sure the playlist exists 98 | rp(options) 99 | .then(res => { 100 | var found = false; 101 | if (res.items.map(item => item.name).includes(this.playListName)) { 102 | found = true; 103 | } 104 | 105 | return resolve({ 106 | success: true, 107 | val: { 108 | found: found, 109 | val: res.items.find(pl => pl.name == this.playListName) 110 | } 111 | }); 112 | }) 113 | .catch(error => { 114 | return reject({ 115 | success: false, 116 | message: "error when sending the request to ifPlayListExists, error code: " + 117 | error 118 | }); 119 | }); 120 | }); 121 | }; 122 | 123 | /** 124 | * check if paltlist already been created or not, if not created, create one and return the id of pl 125 | * if created before, it will get the id from pl 126 | * create_playlist : create default playlist with PlayListName 127 | * => Promise 128 | */ 129 | this.createPlaylist = async () => { 130 | 131 | var _ = await this.getPlayLists().catch(err => err); 132 | if (_.success) { 133 | return new Promise((resolve, reject) => { 134 | // console.debug(_.val.map(elem => elem.name), _.val.map(elem => elem.name).includes(this.playListName), this.playListName); 135 | // if the playlist is already been created 136 | if (_.val.map(elem => elem.name).includes(this.playListName)) { 137 | console.debug("playlist already exists " + this.playListName); 138 | var res = _.val.find(elem => { 139 | // console.debug(elem.name, this.playListName, elem.name == this.playListName) 140 | return elem.name == this.playListName 141 | }); 142 | return resolve({ 143 | success: true, 144 | val: res.id 145 | }); 146 | } else { 147 | // create playlist 148 | console.debug("create playlist " + this.playListName); 149 | var options = { 150 | method: "POST", 151 | url: "https://api.spotify.com/v1/users/" + this.userID + "/playlists", 152 | body: JSON.stringify({ 153 | name: this.playListName, 154 | public: true 155 | }), 156 | dataType: "json", 157 | headers: { 158 | Authorization: "Bearer " + this.accessToken, 159 | "Content-Type": "application/json" 160 | } 161 | }; 162 | 163 | // send request to API 164 | rp(options) 165 | .then(res => { 166 | res = JSON.parse(res); 167 | resolve({ 168 | "success": true, 169 | val: res.id 170 | }); 171 | }) 172 | .catch(error => { 173 | return reject({ 174 | success: false, 175 | message: "error when sending the request to create PlayList, error code: " + 176 | error 177 | }); 178 | }); 179 | 180 | } 181 | }); 182 | } else { 183 | return new Promise((resolve, reject) => { 184 | return reject({ 185 | success: false, 186 | "message": _.message 187 | }); 188 | }); 189 | } 190 | }; 191 | 192 | /** 193 | * during spotify searching, it is more accurate to get the official name that spotify recorded 194 | * the search return a new promise with artist offical name 195 | * String -> String -> Promise 196 | */ 197 | this.getArtistName = async artist => { 198 | var options = { 199 | url: "https://api.spotify.com/v1/search?q=" + 200 | encodeURIComponent(artist) + 201 | "&type=artist", 202 | headers: { 203 | Authorization: "Bearer " + this.accessToken 204 | }, 205 | json: true 206 | }; 207 | 208 | // search for the artist 209 | return new Promise((resolve, reject) => { 210 | rp(options) 211 | .then(res => { 212 | if (res.artists && res.artists.items.length != 0) { 213 | // console.debug("found " + JSON.stringify(res.artists.items[0])) 214 | return resolve({ 215 | success: true, 216 | found: true, 217 | val: res.artists.items[0].name 218 | }); 219 | } 220 | return resolve({ 221 | success: true, 222 | found: false, 223 | val: artist 224 | }); 225 | }) 226 | .catch(error => { 227 | return reject({ 228 | success: false, 229 | message: "error when sending the request to getArtistOfficalName, error: " + 230 | error 231 | }); 232 | }); 233 | }); 234 | }; 235 | 236 | this.getPlayLists = async () => { 237 | var options = { 238 | url: "https://api.spotify.com/v1/users/" + this.userID + "/playlists", 239 | headers: { 240 | Authorization: "Bearer " + this.accessToken 241 | }, 242 | json: true 243 | }; 244 | 245 | return new Promise((resolve, reject) => { 246 | // if this function been used before userID/accessToken defined, rej 247 | if (this.userID === undefined || this.accessToken === undefined) { 248 | return reject({ 249 | success: false, 250 | message: "userID/accessToken is not initlized yet, code error" 251 | }); 252 | } 253 | 254 | // go through all playlist, make sure the playlist exists 255 | rp(options).then(res => { 256 | return resolve({ 257 | success: true, 258 | val: res.items 259 | }) 260 | }); 261 | }); 262 | }; 263 | 264 | /** 265 | * get_song_uri : get the uri match to track , which will be used in when add music 266 | * String -> String -> Promise 267 | * => Promise with uri as value 268 | */ 269 | this.getSongURI = async (track, artist) => { 270 | artist = await this.getArtistName(artist).catch(err => err); 271 | if (!artist.success) { 272 | return new Promise((resolve, reject) => { 273 | return reject({ 274 | success: false, 275 | message: artist.message 276 | }); 277 | }); 278 | } 279 | 280 | if (!artist.found) { 281 | return new Promise((resolve, reject) => { 282 | return resolve({ 283 | success: true, 284 | found: false, 285 | val: undefined 286 | }) 287 | }); 288 | } 289 | 290 | artist = artist.val 291 | 292 | var options = { 293 | url: `https://api.spotify.com/v1/search?q=track${encodeURIComponent( 294 | ":" + track + " " 295 | )}artist${encodeURIComponent(":" + artist)}` + "&type=track", 296 | headers: { 297 | Authorization: "Bearer " + this.accessToken 298 | }, 299 | json: true, 300 | resolveWithFullResponse: true 301 | }; 302 | // console.log(options.url); 303 | 304 | return new Promise((resolve, reject) => { 305 | rp(options).then(res => { 306 | body = res.body; 307 | 308 | // if any of these values are undefined, it means the function is won't return any valid res 309 | if ([body.tracks, body.tracks.items].includes(undefined)) { 310 | return reject({ 311 | success: false, 312 | message: "one of the val for finding SongURI is undefined : " + [body.tracks, body.tracks.items] 313 | }); 314 | } 315 | 316 | if (body.tracks.items.length == 0) { 317 | return resolve({ 318 | success: true, 319 | found: false, 320 | val: undefined 321 | }); 322 | return; 323 | } 324 | 325 | var uri = body.tracks.items[0].uri; 326 | return resolve({ 327 | success: true, 328 | found: true, 329 | val: uri 330 | }); 331 | }); 332 | }); 333 | }; 334 | /** 335 | * @param {*} arr list of song singer 336 | * called right after 337 | */ 338 | this.getSongsURI = async arr => { 339 | var passed = []; 340 | var fail = []; 341 | var uris = []; 342 | 343 | for (i in arr) { 344 | element = arr[i]; 345 | var _ = await this.getSongURI(element[0], element[1]).catch(err => err); 346 | if (!_.success || !_.found) { 347 | fail.push(element); 348 | } 349 | 350 | if (_.success && _.found) { 351 | passed.push(element); 352 | uris.push(_.val); 353 | } 354 | } 355 | 356 | return new Promise((resolve, reject) => { 357 | return resolve({ 358 | success: true, 359 | val: { 360 | passed, 361 | fail, 362 | uris 363 | } 364 | }); 365 | }); 366 | }; 367 | 368 | /** 369 | * @param songs list of track uri 370 | */ 371 | this.addSongsToPlaylist = async (songs) => { 372 | 373 | if (songs.length == 0) { 374 | return new Promise((resolve, reject) => { 375 | resolve({ 376 | success: true, 377 | val: null 378 | }) 379 | }); 380 | 381 | } 382 | 383 | var options = { 384 | method: "POST", 385 | url: "https://api.spotify.com/v1/users/" + 386 | this.userID + 387 | "/playlists/" + 388 | this.playListID + 389 | "/tracks", 390 | headers: { 391 | Authorization: "Bearer " + this.accessToken, 392 | "Content-Type": "application/json" 393 | }, 394 | body: JSON.stringify({ 395 | uris: songs 396 | }) 397 | }; 398 | return new Promise((resolve, reject) => { 399 | rp(options).then(body => { 400 | if (body.statusCode / 500 >= 1) { 401 | return reject({ 402 | success: false, 403 | message: "encounter error when add songs to playlist : " + songs 404 | }); 405 | } else { 406 | return resolve({ 407 | success: true, 408 | val: null 409 | }); 410 | } 411 | }).catch(err => { 412 | console.debug(err.message); 413 | return reject({ 414 | success: false, 415 | message: "encounter error when add songs to playlist : " + err 416 | }); 417 | }); 418 | }); 419 | }; 420 | 421 | /** 422 | * __init__ function for Spotify 423 | * outter class should only use this method 424 | */ 425 | } 426 | 427 | async function init(accessToken, source) { 428 | 429 | sp = new Spotify(accessToken, source); 430 | var _ = await sp.getUserID().catch(err => err); 431 | if (!_.success) { 432 | return new Promise((resolve, reject) => { 433 | reject({ 434 | success: false, 435 | message: _.message 436 | }) 437 | }); 438 | } 439 | 440 | console.debug("got userID " + JSON.stringify(_)); 441 | 442 | if (_.success) { 443 | sp.userID = _.val; 444 | } else { 445 | return new Promise((resolve, reject) => { 446 | return reject({ 447 | success: false, 448 | message: _.message 449 | }); 450 | }); 451 | } 452 | 453 | var _ = await sp.createPlaylist().catch(err => err); 454 | console.debug("got playListID" + JSON.stringify(_)); 455 | if (_.success) { 456 | sp.playListID = _.val; 457 | } else { 458 | console.debug(_.message); 459 | } 460 | 461 | return new Promise((resolve, reject) => { 462 | return resolve({ 463 | success: true, 464 | val: sp 465 | }); 466 | }); 467 | } 468 | 469 | // accessToken = 470 | // "BQAQCxsa8Ve0W4qzZX_x4Gq9fwed3r2emSZilWeTY17Ipncab4kEZKIWAQ5T33hnP_vzmYbtpWl_wxwysEmDVdsPjo1r64b3ovMt2r4ByyNFGGKoruW4ij5IDrjGZT3RWEUXfVwM5dmIfjGd6E5cfPAxXBLcu2F4VqcUbNG7liOY000N2GvnaNylP5ouwdk--v6OElHWMsUsaA"; 471 | 472 | // // var obj = init(accessToken, "tmp").then(res => { 473 | // // console.debug(res); 474 | // // }); 475 | 476 | // (async () => { 477 | // var sp = await init(accessToken, "test1"); 478 | // console.log(sp); 479 | // if (sp.success) { 480 | // sp = sp.val; 481 | // } else { 482 | // return; 483 | // } 484 | // songs_artist = [ 485 | // ["十年", "陈奕迅"], 486 | // ["God's Plan", "Drake"] 487 | // // ["kdjsfksjdfn", "陈奕迅"], 488 | // // ["pressure", "RL Grime"] 489 | // ]; 490 | // var name = await sp.getArtistName(songs_artist[1]).catch(err => err); 491 | // if (name.success) { 492 | // name = name.val; 493 | // } else { 494 | // console.debug(name.message); 495 | // return; 496 | // } 497 | 498 | // console.debug(name); 499 | 500 | 501 | // var uris = await sp.getSongsURI(songs_artist).catch(err => err); 502 | // if (uris.success) { 503 | // console.debug(uris.val); 504 | 505 | // } else { 506 | // console.debug(uris.message); 507 | // } 508 | 509 | // var _ = await sp.addSongsToPlaylist(uris.val.uris).catch(err => err); 510 | // if (_.success) { 511 | // console.debug("done"); 512 | // } else { 513 | // console.debug(_.message); 514 | // } 515 | 516 | 517 | // })(); --------------------------------------------------------------------------------