├── .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)",
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 |
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 |
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 | 
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 | 
22 | - [ ] Create Application (name whatever you want, I personally recommand 'Xiami_connection' for example)
23 | 
24 | - [ ] after you successfully create an application, you will see Client_id and Client_Secret, keep these two information, `Us your owns`
25 | 
26 | 
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)': 'ConKiThe 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 | // })();
--------------------------------------------------------------------------------