获得你收藏歌曲列表链接的办法:点击虾米首页顶栏导航上的「我的音乐」,再点击点击「音乐库」下面的 Tab 「收藏的歌曲」得到的链接即为你收藏歌曲列表的链接
27 |├── .gitignore
├── README.md
├── XiamiList
├── __init__.py
├── grabbot.py
├── tips.py
└── xiami.py
├── app.py
├── app.ui
├── images.qrc
├── images_qr.py
├── mac.spec
├── requirements.txt
├── static
├── app.icns
├── favicon.ico
├── loading.gif
└── xiami.js
├── templates
├── home.html
└── xml.html
├── ui.py
├── web.py
└── windows.spec
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.kgl
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | env/
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | #*.manifest
32 | #*.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 |
46 | # Translations
47 | *.mo
48 | *.pot
49 |
50 | # Django stuff:
51 | *.log
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # PyBuilder
57 | target/
58 | /.python-version
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ExportXiamiList
2 |
3 | 导出虾米歌单,另存为 .kgl 文件,方便倒入到网易云。
4 |
5 | > 注:
6 | > 由于歌曲歌名在不同平台的差异,以及网易云音乐内部的处理机制,导入歌曲不可能完全和虾米平台。
7 |
8 | ## 使用方法
9 |
10 | 复制「我收藏的歌曲」或者歌单链接到输入框。抓取完成后在网易云音乐的「导入歌单」中,将 `kgl` 文件上传导入。
11 |
12 | > 获取「我收藏的歌曲」链接方法:
13 | > 点击虾米首页顶栏导航上的「我的音乐」,再点击点击「音乐库」下面的 Tab 「收藏的歌曲」得到的链接即为你收藏歌曲列表的链接。
14 |
15 | - 单文件程序版
16 |
17 | Windows [下载](https://github.com/fyl00/ExportXiamiList/releases/download/v1.0.0/XiamiList.zip)
18 |
19 | Mac [下载](https://github.com/fyl00/ExportXiamiList/releases/download/v1.0.0/XiamiList.dmg)
20 |
21 | - 在线版(已失效)
22 |
23 | 因为[新浪云](http://t.cn/RqJ2vND)没有免费套餐了,虾米又禁了国外 IP,所以暂时没地方放了。
24 |
25 |
26 | # TODO
27 |
28 | 有机会把 QQ 音乐和网易云音乐的歌单也解析出来。
29 |
30 |
--------------------------------------------------------------------------------
/XiamiList/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fyl00/ExportXiamiList/b769740e4613f58ff29381f1e8865adbf6368ac2/XiamiList/__init__.py
--------------------------------------------------------------------------------
/XiamiList/grabbot.py:
--------------------------------------------------------------------------------
1 | # python 3
2 |
3 | import requests
4 | import logging
5 | from random import randint
6 | from time import sleep
7 | from functools import wraps
8 |
9 | USER_AGENTS = ['Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0',
10 | 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
11 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko',
12 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) \
13 | Chrome/50.0.2661.102 Safari/537.36',
14 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'
15 | ]
16 |
17 | def retry(ExceptionToCheck, tries=3, delay=3, backoff=1):
18 | """Retry calling the decorated function using an exponential backoff.
19 |
20 | original from: http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
21 |
22 | :param ExceptionToCheck: the exception to check. may be a tuple of
23 | exceptions to check
24 | :type ExceptionToCheck: Exception or tuple
25 | :param tries: number of times to try (not retry) before giving up
26 | :type tries: int
27 | :param delay: initial delay between retries in seconds
28 | :type delay: int
29 | :param backoff: backoff multiplier e.g. value of 2 will double the delay
30 | each retry
31 | :type backoff: int
32 | """
33 |
34 | def deco_retry(f):
35 |
36 | @wraps(f)
37 | def f_retry(*args, **kwargs):
38 | mtries, mdelay = tries, delay
39 | while mtries > 1:
40 | try:
41 | return f(*args, **kwargs)
42 | except ExceptionToCheck as e:
43 | msg = "Retry (%s) in %d seconds. (ERROR: %s) " \
44 | % (f.__name__.upper(), mdelay, str(e))
45 | logging.warning(msg)
46 |
47 | sleep(mdelay)
48 | mtries -= 1
49 | mdelay *= backoff
50 | return f(*args, **kwargs)
51 |
52 | return f_retry # true decorator
53 |
54 | return deco_retry
55 |
56 |
57 | class GrabBot(object):
58 | """ Simple bot to get web content """
59 |
60 | def __init__(self, proxy=None):
61 | self.proxies = {'http': proxy,
62 | 'https': proxy}
63 |
64 | @retry(requests.exceptions.RequestException, backoff=2)
65 | def _get(self, url, **kwargs):
66 | ua = USER_AGENTS[randint(0, len(USER_AGENTS) - 1)]
67 | return requests.get(url, timeout=(10, 60), proxies=self.proxies,
68 | headers={'User-Agent': ua}, **kwargs)
69 |
70 | @retry(requests.exceptions.RequestException, backoff=2)
71 | def _post(self, url, data):
72 | ua = USER_AGENTS[randint(0, len(USER_AGENTS) - 1)]
73 | return requests.post(url, data=data, timeout=(10, 60),
74 | proxies=self.proxies, headers={'User-Agent': ua})
75 |
76 | def get(self, url, **kwargs):
77 | r = None
78 | try:
79 | r = self._get(url, **kwargs)
80 | except requests.exceptions.RequestException as err:
81 | logging.warning('Failed to connect URL(%s), %s' % (url, err))
82 | return r
83 |
84 | def post(self, url, data):
85 | r = None
86 | try:
87 | r = self._post(url, data=data)
88 | except requests.exceptions.RequestException as err:
89 | logging.warning('Failed to post data to URL(%s), %s' % (url, err))
90 | return r
91 |
--------------------------------------------------------------------------------
/XiamiList/tips.py:
--------------------------------------------------------------------------------
1 | """ tips """
2 |
3 | GET_LINK = """**重要提示**
4 | 获得你收藏歌曲列表链接的办法:
5 | 点击虾米首页顶栏导航上的「我的音乐」,
6 | 再点击点击「音乐库」下面的 Tab 「收藏的歌曲」得到的链接即为你收藏歌曲列表的链接。
7 | """
8 |
9 | LINK_ERROR_TIPS = """歌单链接错误,请重新校对:
10 | 红心歌曲链接例子:http://www.xiami.com/space/lib-song/u/2200240
11 | 歌单链接:http://www.xiami.com/collect/29594456
12 | """
--------------------------------------------------------------------------------
/XiamiList/xiami.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import re
4 |
5 | from lxml import html, etree
6 | from .grabbot import GrabBot
7 | from .tips import LINK_ERROR_TIPS
8 |
9 |
10 | class XiamiLink(object):
11 |
12 | def __init__(self, url):
13 | self.url = url
14 |
15 | @property
16 | def is_collect(self):
17 | user_regx = re.search(r"(?Phttp://www.xiami.com/space/lib-song/u/\d+)\D*", self.url)
18 | collect_regx = re.search(r"(?Phttp://www.xiami.com/collect/\d+)\D*", self.url)
19 | if (not user_regx) and (not collect_regx):
20 | print(LINK_ERROR_TIPS)
21 | elif user_regx:
22 | self.url = user_regx.group("link")
23 | return False
24 | elif collect_regx:
25 | self.url = collect_regx.group("link")
26 | return True
27 | return None
28 |
29 |
30 | class XiamiHandle(object):
31 | def __init__(self, pagecount=None):
32 | self.songs = []
33 | self.tree = None
34 | self.pagecount = pagecount
35 | self.pagination = 0
36 | self.isPageExistedSong = True
37 |
38 | def get_u_song(self):
39 | song_nodes = self.tree.xpath(".//table[@class='track_list']//tr")
40 | # print(etree.tostring(song_nodes[1]))
41 | if len(song_nodes):
42 | for node in song_nodes:
43 |
44 | name_nodes = node.xpath("td[@class='song_name']/a/@title")
45 | artist_nodes = node.xpath("td[@class='song_name']/a[@class='artist_name']/@title")
46 | if name_nodes and artist_nodes:
47 | # and name_tmp[0] not in ["高清MV", "音乐人"]
48 | song_name = name_nodes[0]
49 | artist_name = "、".join(artist_nodes)
50 | info = artist_name + " - " + song_name
51 | print("-> %s" % info)
52 | self.songs.append(info)
53 | else:
54 | print("获取歌曲失败. %s" % [node for node in name_nodes])
55 |
56 | return True
57 | else:
58 | print("第 %s 页没有数据." % self.pagination)
59 | return False
60 |
61 | def get_collect_song(self):
62 | song_nodes = self.tree.xpath(".//div[@class='quote_song_list']//li")
63 | for node in song_nodes:
64 | # check the song's checkbox
65 | if node.xpath("//span[@class='chk']/input[@checked]"):
66 | song_info_nodes = node.xpath("div//span[@class='song_name']/a")
67 | if song_info_nodes and len(song_info_nodes) >= 2:
68 | song_name = song_info_nodes[0].text.strip()
69 | artist_names = [song_info_nodes[i].text.strip() for i in range(1, len(song_info_nodes))
70 | if song_info_nodes[i].text.strip() != "MV"]
71 | if len(artist_names) > 0:
72 | info = "、".join(artist_names) + " - " + song_name
73 | print("-> %s" % info)
74 | self.songs.append(info)
75 | else:
76 | song_name_nodes = node.xpath("div//span[@class='song_name']")
77 | if song_name_nodes:
78 | song_name = song_name_nodes[0].text.replace("--", "").strip()
79 | artist_nodes = song_name_nodes[0].xpath("a")
80 | if artist_nodes:
81 | artist_names = [artist_node.text for artist_node in artist_nodes]
82 | info = "、".join(artist_names) + " - " + song_name
83 | print("-> %s" % info)
84 | self.songs.append(info)
85 |
86 | def create_songlist_xml(self, listname):
87 |
88 | root = etree.Element("List", ListName=listname)
89 | for song in self.songs:
90 | songname = song+".mp3"
91 | file_node = etree.SubElement(root, "File")
92 | name_node = etree.SubElement(file_node, "FileName")
93 | name_node.text = songname
94 | # etree.ElementTree(root).write("xiami.kgl",
95 | # xml_declaration=True,
96 | # encoding="utf8",
97 | # pretty_print=True)
98 | return etree.tounicode(root)
99 |
100 | def get_list(self, url):
101 | spider = GrabBot()
102 | link = XiamiLink(url)
103 | response = spider.get(url)
104 | self.tree = html.fromstring(response.text)
105 | print("*** START ***\n开始抓取歌单:%s" % url)
106 | if not link.is_collect:
107 | xmllistname = u'虾米红心'
108 | while self.isPageExistedSong:
109 | self.pagination += 1
110 | page_url = link.url + "/page/" + str(self.pagination)
111 |
112 | print("第 %s 页: %s" % (self.pagination, page_url))
113 | try:
114 | resp = spider.get(page_url)
115 | self.tree = html.fromstring(resp.text)
116 | self.isPageExistedSong = self.get_u_song()
117 | except Exception as err:
118 | print("在抓取 %s 页时发生错误:\n\t%s" % (self.pagination, err))
119 | return self.create_songlist_xml(xmllistname)
120 | pass
121 |
122 | else:
123 | name_nodes = self.tree.xpath(".//div[@class='info_collect_main']/h2")
124 | xmllistname = "collect"
125 | if name_nodes:
126 | xmllistname = name_nodes[0].text.strip()
127 |
128 | self.get_collect_song()
129 |
130 | xmlstr = self.create_songlist_xml(xmllistname)
131 | return xmlstr
132 |
133 |
134 | def xiamisonglist(url):
135 | result = XiamiHandle().get_list(url)
136 | return result
137 |
138 | if __name__ == "__main__":
139 | # XiamiHandle().create_songlist_xml()
140 | user_url = "http://www.xiami.com/space/lib-song/u/2200240?spm=a1z1s.6928797.1561534513.1.U6HtZZ"
141 | collect_url = "http://www.xiami.com/collect/29594456"
142 | XiamiHandle().get_list(user_url)
143 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | # python3
2 | # author: fyl00
3 | # source: https://github.com/fyl00/ExportXiamiList
4 |
5 | import logging
6 | import re
7 | import sys
8 |
9 | from PyQt5.QtCore import QObject, QThread, pyqtSignal
10 | from PyQt5.QtGui import QTextCursor, QIcon
11 | from PyQt5.QtWidgets import QMainWindow, QApplication, QMessageBox, QFileDialog
12 | from lxml import etree
13 |
14 | from XiamiList.tips import *
15 | from XiamiList.xiami import XiamiHandle, XiamiLink
16 | from ui import Ui_MainWindow
17 | import images_qr
18 |
19 |
20 | # 打印输出到 logTextEdit
21 | class QtLogHandler(logging.Handler):
22 | def __init__(self):
23 | logging.Handler.__init__(self)
24 |
25 | def emit(self, record):
26 | record = self.format(record)
27 | if record:
28 | EmittingStream.stdout().write('%s\n' % record)
29 |
30 |
31 | class EmittingStream(QObject):
32 | _stdout = None
33 | _stderr = None
34 | textWritten = pyqtSignal(str)
35 |
36 | def write(self, text):
37 | if not self.signalsBlocked():
38 | self.textWritten.emit(str(text))
39 |
40 | def flush(self):
41 | pass
42 |
43 | def fileno(self):
44 | return -1
45 |
46 | @staticmethod
47 | def stdout():
48 | if not EmittingStream._stdout:
49 | EmittingStream._stdout = EmittingStream()
50 | sys.stdout = EmittingStream._stdout
51 | return EmittingStream._stdout
52 |
53 | @staticmethod
54 | def stderr():
55 | if not EmittingStream._stderr:
56 | EmittingStream._stderr = EmittingStream()
57 | sys.stderr = EmittingStream._stderr
58 | return EmittingStream._stderr
59 |
60 |
61 | # 后台抓取,防止界面未响应
62 | class XiamiThread(QThread):
63 |
64 | finished = pyqtSignal(str)
65 |
66 | def __init__(self, url):
67 | QThread.__init__(self)
68 | self.url = url
69 |
70 | def run(self):
71 | xmlstr = XiamiHandle().get_list(self.url)
72 | self.finished.emit(xmlstr)
73 |
74 |
75 | # 界面窗口
76 | class AppWindow(QMainWindow):
77 |
78 | def __init__(self):
79 | QMainWindow.__init__(self)
80 | self.ui = Ui_MainWindow()
81 | self.ui.setupUi(self)
82 | self._enbale_source_link()
83 | self.ui.startButton.clicked.connect(self.click_start_button)
84 | EmittingStream.stdout().textWritten.connect(self._logout)
85 | EmittingStream.stderr().textWritten.connect(self._logout)
86 |
87 | self.ui.linkLineEdit.setPlaceholderText("请输入歌单链接")
88 | self.ui.linkLineEdit.setFocus()
89 |
90 | self._logout(GET_LINK)
91 |
92 | # Storing a reference to the thread after it's been created
93 | # http://stackoverflow.com/questions/15702782/qthread-destroyed-while-thread-is-still-running
94 | self.threads = []
95 |
96 | def click_start_button(self):
97 | url = self.ui.linkLineEdit.text()
98 | if not self._check_url(url):
99 | return
100 | thread = XiamiThread(url)
101 | self.threads.append(thread)
102 | thread.finished.connect(self._task_finished)
103 | thread.start()
104 | self.ui.startButton.setDisabled(True)
105 |
106 | def _check_url(self, url):
107 | link = XiamiLink(url)
108 | if link.is_collect is None:
109 | title = "链接格式错误"
110 | QMessageBox.critical(self, title, LINK_ERROR_TIPS)
111 | return False
112 | return True
113 |
114 | def _task_finished(self, value):
115 | self._save_xml(value)
116 | self.ui.startButton.setDisabled(False)
117 |
118 | def _enbale_source_link(self):
119 | link_text = "源码:GitHub"
120 | self.ui.sourceLabel.setText(link_text)
121 | self.ui.sourceLabel.setOpenExternalLinks(True)
122 |
123 | def _logout(self, outstr):
124 | cursor = self.ui.logTextEdit.textCursor()
125 | cursor.insertText(outstr)
126 | self.ui.logTextEdit.moveCursor(QTextCursor.End)
127 |
128 | def _save_xml(self, xmlstr):
129 | options = QFileDialog.Options()
130 | options |= QFileDialog.DontUseNativeDialog
131 | filename, _ = QFileDialog.getSaveFileName(self, "QFileDialog.getSaveFileName()",
132 | "songs.kgl", "Kugou/Netease Files (*.kgl)",
133 | options=options)
134 | if filename:
135 | r = re.search("\.kgl$", filename)
136 | if not r:
137 | filename = "%s.kgl" % filename
138 | print("** 导出文件位置:%s" % filename)
139 | root = etree.fromstring(xmlstr)
140 | etree.ElementTree(root).write(filename,
141 | xml_declaration=True,
142 | encoding="utf8",
143 | pretty_print=True)
144 |
145 |
146 | if __name__ == "__main__":
147 | app = QApplication(sys.argv)
148 | window = AppWindow()
149 | app.setWindowIcon(QIcon(':/static/favicon.ico'))
150 | window.show()
151 | sys.exit(app.exec_())
152 |
--------------------------------------------------------------------------------
/app.ui:
--------------------------------------------------------------------------------
1 |
2 |
获得你收藏歌曲列表链接的办法:点击虾米首页顶栏导航上的「我的音乐」,再点击点击「音乐库」下面的 Tab 「收藏的歌曲」得到的链接即为你收藏歌曲列表的链接
27 |{{log}}点击此处 将已抓取的歌单另存为 .kgl 文件后就可以导入网易云音乐了。
3 | {% endif %} 4 | {% if xml %} 5 |