├── .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 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 579 10 | 573 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | Arial 22 | 10 23 | 24 | 25 | 26 | 虾米歌单抓取工具 27 | 28 | 29 | 30 | 31 | 12 32 | 33 | 34 | 35 | 36 | 37 | 12 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 0 47 | 30 48 | 49 | 50 | 51 | 52 | 300 53 | 50 54 | 55 | 56 | 57 | 58 | Arial 59 | 10 60 | 50 61 | false 62 | false 63 | 64 | 65 | 66 | START 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 华文细黑 78 | 10 79 | 50 80 | true 81 | false 82 | false 83 | 84 | 85 | 86 | 源码:Github 87 | 88 | 89 | true 90 | 91 | 92 | Qt::AlignCenter 93 | 94 | 95 | false 96 | 97 | 98 | 5 99 | 100 | 101 | false 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /images.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | static/favicon.ico 4 | 5 | -------------------------------------------------------------------------------- /images_qr.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.8.0) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x02\x21\ 13 | \x00\ 14 | \x00\x10\xbe\x78\x9c\xed\xd6\x4f\x6c\x0c\x61\x18\xc7\xf1\x67\x15\ 15 | \xab\x29\x56\x55\xea\xd0\xd0\x36\x13\x22\x71\x71\x53\x89\xd8\x86\ 16 | \x04\x37\x42\x22\x71\x14\x24\x5c\x1c\x5c\x49\x7a\xa8\xc4\x49\xda\ 17 | \x38\x68\x83\x9b\x48\x84\x83\x8b\xe0\x66\x09\x71\x90\xe0\xe2\xe2\ 18 | \xef\x90\xb8\x48\xc8\x3a\x68\xa9\xd1\xf1\x7d\x3a\xcf\xea\x74\xcc\ 19 | \xee\xcc\xee\xb6\x15\xc9\xbc\x9b\xcf\x66\xb3\x33\xf3\xfe\x9e\x7d\ 20 | \xdf\x77\xde\x59\x91\x1c\xaf\x9e\x1e\xd1\x77\xb9\xb6\x42\xa4\x53\ 21 | \x44\x36\x80\xaf\xa4\x5f\x82\xef\xa7\x1a\xc7\x3a\xda\x02\x59\xcb\ 22 | \x5a\xd6\xa6\x9b\x73\x76\xa2\x62\x3b\xce\x63\x04\x17\x66\x81\xf6\ 23 | \x33\x88\x8d\x95\x8c\x1a\xf9\xeb\xf0\x0a\xfe\x1c\xb8\x89\xc5\x09\ 24 | \xf9\x45\x8c\xcd\x51\x7e\x09\xad\x75\xe4\xbf\xc4\x09\x1c\x6f\xd0\ 25 | \x31\x9c\xc2\xe7\x06\xf3\x6b\x9e\x9f\x72\x3d\xad\xc5\xfb\x2c\xff\ 26 | \x9f\xe5\x2f\xc3\xa5\x66\xf3\xcd\x82\xb4\xb5\x84\xce\xcf\x59\x0d\ 27 | \x67\x70\x19\xf9\x26\xf2\x77\xa1\xcf\xfa\x4c\x93\xbf\x09\x47\xad\ 28 | \x8f\x16\x2c\xaf\x75\x6d\x8a\xfc\x3d\x78\x83\xd3\xe8\xc5\xa2\x84\ 29 | \xbe\xfa\xf1\x05\xc3\xe8\xa8\xf4\x93\x50\x73\xad\xfc\x55\x78\x62\ 30 | \xc7\xde\xe1\x06\x8e\xd8\x98\xac\x41\x9b\x9d\xdf\x6a\xb5\xed\xc4\ 31 | \x38\x3c\xeb\x6b\x6b\x93\xbf\x5f\x9d\xc4\x2f\x67\x7a\x4f\xd3\xcf\ 32 | \x5f\xf1\x16\x0f\xed\x1a\x75\x0f\xcf\x23\xe7\x7e\xc0\x01\x2c\x8c\ 33 | \xab\x21\x69\xfd\xdb\xf1\x95\xb8\xeb\xd4\xb7\xef\x86\x95\x31\x80\ 34 | \xce\x68\x0d\x49\xf9\x91\x75\xf5\xb4\x89\x1a\x26\x71\x0b\xeb\x63\ 35 | \xfa\x4e\x93\xaf\xf4\x59\x7a\xdb\xe6\xb6\x91\x1a\x3e\x61\x7f\x42\ 36 | \xfe\x47\x5c\x74\x66\x3e\xcb\x47\x71\x08\x4b\x9c\x60\x3d\x1e\xc6\ 37 | \x23\x4c\xd4\x91\xfd\x0c\x3b\x9c\xc8\x7e\x62\xf9\xdb\xf0\x2d\xe1\ 38 | \x7a\xad\xef\xaa\xcd\x83\xae\x67\xbd\xb7\x0e\xe2\x0a\x5e\x38\xc1\ 39 | \x7a\xfc\x11\x73\xdd\x4f\x5c\xaf\x8c\x7b\x95\xf9\xef\xc2\x03\x27\ 40 | \xb8\x6f\xc6\xaa\x18\xb7\xdf\xfb\x1a\xfb\x42\x73\xa2\x7b\xcc\x6a\ 41 | \x6c\xc6\x6e\x9c\x0b\x8d\x8b\x3e\x83\xf5\x3f\x50\x21\x9a\x1b\x33\ 42 | \xb7\x5d\x36\x0e\xc5\x04\xfa\x3f\xad\x37\xae\xbf\xd0\x5c\x6a\xad\ 43 | \xba\x67\xed\xb5\xfa\x62\xb3\x67\xbb\x59\xbe\xee\x37\x77\xb0\x25\ 44 | \x6e\xbc\xe7\x21\x7f\x29\xda\xe7\x3b\x3b\x6b\xff\x57\xf3\xab\x34\ 45 | \x57\xa4\x1b\x85\x3f\xfc\xc9\x99\x86\xbc\x82\xfb\x18\xf7\xbd\xa2\ 46 | \x5b\xe2\xb8\x2b\x79\xb4\xb8\x65\xc9\xb9\xdf\x21\x53\xf2\xae\x94\ 47 | \x0a\x65\xf1\xba\x7d\xdf\x1b\xc0\x5f\x39\xbf\x01\xc2\xc8\x6c\x31\ 48 | \ 49 | " 50 | 51 | qt_resource_name = b"\ 52 | \x00\x06\ 53 | \x07\xaa\x8a\xf3\ 54 | \x00\x73\ 55 | \x00\x74\x00\x61\x00\x74\x00\x69\x00\x63\ 56 | \x00\x0b\ 57 | \x0a\xb8\x56\x7f\ 58 | \x00\x66\ 59 | \x00\x61\x00\x76\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x69\x00\x63\x00\x6f\ 60 | " 61 | 62 | qt_resource_struct = b"\ 63 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 64 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ 65 | \x00\x00\x00\x12\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\ 66 | " 67 | 68 | def qInitResources(): 69 | QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) 70 | 71 | def qCleanupResources(): 72 | QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) 73 | 74 | qInitResources() 75 | -------------------------------------------------------------------------------- /mac.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['app.py'], 8 | pathex=[], 9 | binaries=None, 10 | datas=[], 11 | hiddenimports=['queue'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[''], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher) 18 | 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | 22 | exe = EXE(pyz, 23 | a.scripts, 24 | a.binaries, 25 | a.zipfiles, 26 | a.datas, 27 | name='xiamilist', 28 | debug=False, 29 | strip=False, 30 | upx=True, 31 | console=False , 32 | icon='static/favicon.ico') 33 | 34 | app = BUNDLE(exe, 35 | name='XiamiList.app', 36 | icon='static/app.icns', 37 | bundle_identifier=None, 38 | info_plist={ 39 | 'NSHighResolutionCapable': 'True' 40 | }, 41 | ) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyqt5==5.8.2 2 | lxml==3.7.3 3 | requests==2.13.0 4 | pyinstaller==3.2.1 5 | flask==0.7.2 6 | -------------------------------------------------------------------------------- /static/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyl00/ExportXiamiList/b769740e4613f58ff29381f1e8865adbf6368ac2/static/app.icns -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyl00/ExportXiamiList/b769740e4613f58ff29381f1e8865adbf6368ac2/static/favicon.ico -------------------------------------------------------------------------------- /static/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyl00/ExportXiamiList/b769740e4613f58ff29381f1e8865adbf6368ac2/static/loading.gif -------------------------------------------------------------------------------- /static/xiami.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fyl00/ExportXiamiList/b769740e4613f58ff29381f1e8865adbf6368ac2/static/xiami.js -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 导出虾米歌单 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 |
14 |
15 |

导出虾米歌单

16 | 源码 17 | | 18 | @fyl00 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |

获得你收藏歌曲列表链接的办法:
点击虾米首页顶栏导航上的「我的音乐」,再点击点击「音乐库」下面的 Tab 「收藏的歌曲」得到的链接即为你收藏歌曲列表的链接

27 |
28 |
29 | 30 | 31 | 32 | 88 | 89 | -------------------------------------------------------------------------------- /templates/xml.html: -------------------------------------------------------------------------------- 1 | {% if log%} 2 |

{{log}}点击此处 将已抓取的歌单另存为 .kgl 文件后就可以导入网易云音乐了。

3 | {% endif %} 4 | {% if xml %} 5 |
6 | 7 | {{xml|safe}} 8 | 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'app.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.7 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_MainWindow(object): 12 | def setupUi(self, MainWindow): 13 | MainWindow.setObjectName("MainWindow") 14 | MainWindow.resize(579, 573) 15 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 16 | sizePolicy.setHorizontalStretch(0) 17 | sizePolicy.setVerticalStretch(0) 18 | sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) 19 | MainWindow.setSizePolicy(sizePolicy) 20 | font = QtGui.QFont() 21 | font.setFamily("Arial") 22 | font.setPointSize(10) 23 | MainWindow.setFont(font) 24 | self.centralwidget = QtWidgets.QWidget(MainWindow) 25 | self.centralwidget.setObjectName("centralwidget") 26 | self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) 27 | self.verticalLayout.setSpacing(12) 28 | self.verticalLayout.setObjectName("verticalLayout") 29 | self.linkLineEdit = QtWidgets.QLineEdit(self.centralwidget) 30 | font = QtGui.QFont() 31 | font.setPointSize(12) 32 | self.linkLineEdit.setFont(font) 33 | self.linkLineEdit.setObjectName("linkLineEdit") 34 | self.verticalLayout.addWidget(self.linkLineEdit) 35 | self.startButton = QtWidgets.QPushButton(self.centralwidget) 36 | self.startButton.setMinimumSize(QtCore.QSize(0, 30)) 37 | self.startButton.setMaximumSize(QtCore.QSize(300, 50)) 38 | font = QtGui.QFont() 39 | font.setFamily("Arial") 40 | font.setPointSize(10) 41 | font.setBold(False) 42 | font.setWeight(50) 43 | font.setKerning(False) 44 | self.startButton.setFont(font) 45 | self.startButton.setObjectName("startButton") 46 | self.verticalLayout.addWidget(self.startButton, 0, QtCore.Qt.AlignHCenter) 47 | self.logTextEdit = QtWidgets.QTextEdit(self.centralwidget) 48 | self.logTextEdit.setObjectName("logTextEdit") 49 | self.verticalLayout.addWidget(self.logTextEdit) 50 | self.sourceLabel = QtWidgets.QLabel(self.centralwidget) 51 | font = QtGui.QFont() 52 | font.setFamily("华文细黑") 53 | font.setPointSize(10) 54 | font.setBold(False) 55 | font.setItalic(True) 56 | font.setUnderline(False) 57 | font.setWeight(50) 58 | self.sourceLabel.setFont(font) 59 | self.sourceLabel.setScaledContents(True) 60 | self.sourceLabel.setAlignment(QtCore.Qt.AlignCenter) 61 | self.sourceLabel.setWordWrap(False) 62 | self.sourceLabel.setIndent(5) 63 | self.sourceLabel.setOpenExternalLinks(False) 64 | self.sourceLabel.setObjectName("sourceLabel") 65 | self.verticalLayout.addWidget(self.sourceLabel) 66 | MainWindow.setCentralWidget(self.centralwidget) 67 | 68 | self.retranslateUi(MainWindow) 69 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 70 | 71 | def retranslateUi(self, MainWindow): 72 | _translate = QtCore.QCoreApplication.translate 73 | MainWindow.setWindowTitle(_translate("MainWindow", "虾米歌单抓取工具")) 74 | self.startButton.setText(_translate("MainWindow", "START")) 75 | self.sourceLabel.setText(_translate("MainWindow", "源码:Github")) 76 | 77 | -------------------------------------------------------------------------------- /web.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # python 2.7 3 | # env:xiamilist 4 | 5 | from flask import Flask,render_template,request,Response,Markup 6 | from xiamilist import xiami 7 | 8 | 9 | app = Flask(__name__) 10 | 11 | app.debug = False 12 | 13 | 14 | 15 | @app.route('/') 16 | def hello_world(): 17 | return render_template('home.html') 18 | 19 | @app.route('/xml/',methods=['POST','GET']) 20 | def export(): 21 | userLink = request.args.get('XiamiListLink') 22 | methods = request.method 23 | usercontent = xiami.xiamisonglist(userLink) 24 | log = usercontent['log'] 25 | xml = usercontent['xmlContent'] 26 | xml = xml.replace('&',u'、') 27 | return render_template('xml.html',log=log,xml=xml) 28 | 29 | 30 | if __name__ == '__main__': 31 | app.run() 32 | -------------------------------------------------------------------------------- /windows.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['app.py'], 7 | pathex=[], 8 | binaries=None, 9 | datas=[], 10 | hiddenimports=[], 11 | hookspath=[], 12 | runtime_hooks=[], 13 | excludes=[''], 14 | win_no_prefer_redirects=False, 15 | win_private_assemblies=False, 16 | cipher=block_cipher) 17 | pyz = PYZ(a.pure, a.zipped_data, 18 | cipher=block_cipher) 19 | exe = EXE(pyz, 20 | a.scripts, 21 | a.binaries, 22 | a.zipfiles, 23 | a.datas, 24 | name='BulletinParser', 25 | debug=False, 26 | strip=False, 27 | upx=True, 28 | console=False , 29 | icon='static/favicon.ico') --------------------------------------------------------------------------------