#!/usr/bin/env python # -*- coding: utf-8 -*- """ File: fm_api.py Author: tdoly """ import re import json import os import pickle import base64 import logging import requests import rsa import consts import fm_utils from urllib import urlencode logger = logging.getLogger("baidufm") requests.packages.urllib3.disable_warnings() ERROR_MSG = { '-1': '系统错误, 请稍后重试', '0': '登录成功', '1': '您输入的帐号格式不正确', '2': '您输入的帐号不存在', '3': '验证码不存在或已过期,请重新输入', '4': '您输入的帐号或密码有误', '5': '请在弹出的窗口操作,或重新登录', '6': '验证码输入错误', '16': '您的帐号因安全问题已被限制登录', '257': '需要验证码', '100005': '系统错误, 请稍后重试', '120016': '未知错误 120016', '120019': '近期登录次数过多, 请先通过 passport.baidu.com 解除锁定', '120021': '登录失败,请在弹出的窗口操作,或重新登录', '500010': '登录过于频繁,请24小时后再试', '401007': '您的手机号关联了其他帐号,请选择登录' } class APIError(Exception): pass class BaiduFmAPI(object): def __init__(self, username, password): self.session = requests.session() self.username = username self.password = password self.cookies_file = 'cookies' self.token_file = 'token' self.user = {} self.login_data = None # 获取初始cookie self._request(consts.HOST) self.get_fm_user_info() self._initiate() def _initiate(self): if not os.path.exists(consts.HOST_PATH): os.makedirs(consts.HOST_PATH) if not self._load_cookies(): self.user['token'] = self._get_token() if self.password and self.username: self.login() else: self._save_cookies() else: self.user['token'] = self._get_token() def _save_cookies(self): cookies_path = os.path.join(consts.HOST_PATH, self.cookies_file) with open(cookies_path, 'w') as f: pickle.dump(requests.utils.dict_from_cookiejar(self.session.cookies), f) def _load_cookies(self): cookies_path = os.path.join(consts.HOST_PATH, self.cookies_file) if os.path.exists(cookies_path): with open(cookies_path, 'r') as cookies_file: load_cookies = pickle.load(cookies_file) cookies = requests.utils.cookiejar_from_dict(load_cookies) self.session.cookies = cookies if 'BDUSS' in load_cookies: self.user['BDUSS'] = load_cookies['BDUSS'] if 'BAIDUID' in load_cookies: self.user['BAIDUID'] = load_cookies['BAIDUID'] return True else: return False def _get_token(self): """ 获取token :return: """ token = '' token_path = os.path.join(consts.HOST_PATH, self.token_file) if os.path.exists(token_path): with open(token_path, 'r') as token_file: token = token_file.readline() if not token: token_url = consts.URL_TOKEN % fm_utils.timestamp() content = self._request(token_url).content.replace('\'', '\"') token = json.loads(content)['data']['token'] with open(token_path, 'w') as f: f.write(token) logger.info('get token: %s' % token) return token def _get_public_key(self): url = consts.URL_PUBLIC_KEY % (self.user['token'], fm_utils.timestamp()) content = self._request(url).content data = json.loads(content.replace('\'', '"')) return data['pubkey'], data['key'] def _get_captcha(self, code_string): if code_string: logger.info("requiring captcha") url = consts.URL_CAPTCHA % code_string jpeg = self._request(url).content captcha_path = fm_utils.save_captcha(jpeg) else: captcha_path = "" return captcha_path def _login_check(self): url_phoenix = consts.URL_PHOENIX url_login_history = consts.URL_LOGIN_HISTORY % ( self.user['token'], fm_utils.timestamp()) url_login_check = consts.URL_LOGIN_CHECK % ( self.user['token'], fm_utils.timestamp(), self.username) self._request(url_phoenix) self._request(url_login_history) self._request(url_login_check) def login(self, username=None, password=None, verify_code=None): if username and password: self.username = username self.password = password if not self.login_data: self._login_check() pub_key, rsa_key = self._get_public_key() key = rsa.PublicKey.load_pkcs1_openssl_pem(pub_key) pwd_rsa = base64.b64encode(rsa.encrypt(self.password, key)) self.login_data = { 'staticpage': 'http://fm.baidu.com/player/v2Jump.html', 'charset': 'UTF-8', 'token': self.user['token'], 'tpl': 'box', 'subpro': None, 'apiver': 'v3', 'tt': fm_utils.timestamp(), 'codestring': None, 'isPhone': None, 'safeflg': '0', 'u': 'http://fm.baidu.com', 'quick_user': '0', 'logintype': 'dialogLogin', 'logLoginType': 'pc_loginDialog', 'idc': None, 'loginmerge': 'true', 'splogin': 'rate', 'username': self.username, 'password': pwd_rsa, 'verifycode': verify_code, 'mem_pass': 'on', 'rsakey': rsa_key, 'crypttype': 12, 'ppui_logintime': 14929, 'callback': 'parent.bd__pcbs__irpbf3' } else: self.login_data['verifycode'] = verify_code result = self._request(consts.URL_LOGIN, 'post', self.login_data) if not result.ok: raise APIError('Logging failed.') content = result.content # 是否需要验证码 if 'err_no=257' in content or 'err_no=6' in content: codestring = re.findall('codeString=(.*?)&', content)[0] logger.info('need captcha, codeString=%s', codestring) self.login_data['codestring'] = codestring captcha_path = self._get_captcha(codestring) return captcha_path self._check_account_exception(content) try: self.user['BDUSS'] = self.session.cookies['BDUSS'] except Exception as e: logger.error("Get BDUSS: %s", str(e.args)) logger.info('user %s Logged in BDUSS: %s' % (self.username, self.user['BDUSS'])) self._save_cookies() def _check_account_exception(self, content): err_id = re.findall('err_no=([\d]+)', content)[0] if err_id == '0': return try: msg = ERROR_MSG[err_id] except Exception as e: logger.error("_check_account_exception %s", str(e.args)) msg = 'unknown err_id=' + err_id raise APIError(msg) def _params_utf8(self, params): for k, v in params.items(): if isinstance(v, unicode): params[k] = v.encode('utf-8') def _request(self, url, method=None, extra_params=None): params = dict() if extra_params: params.update(extra_params) headers = consts.HEADERS if 'fm.baidu.com' in url: headers['Host'] = "fm.baidu.com" elif 'passport.baidu.com' in url: headers['Host'] = "passport.baidu.com" else: headers['Host'] = ".baidu.com" self._params_utf8(params) if method and method.lower() == 'post': response = self.session.post(url, data=params, verify=True, headers=headers) else: if '?' in url: url = url + urlencode(params) else: url = url + '?' + urlencode(params) response = self.session.get(url, verify=False, headers=headers) return response def get_fm_user_info(self): """ 获取用户信息 :return: """ url = consts.FM_USER_INFO % fm_utils.timestamp() content = self._request(url) return content.json() def get_fm_user_counts(self): """ 获取用户听歌统计和hash_code :return: """ hashcode = self.user.get('hashcode', '') params = dict() params['_'] = fm_utils.timestamp() params['tn'] = 'usercounts' params['hashcode'] = hashcode content = self._request(consts.FM_API, extra_params=params) return content.json() def get_fm_channel_list(self): """ 获取频道信息 :return: """ if 'hashcode' not in self.user: user_count = self.get_fm_user_counts() hash_code = user_count['hash_code'] self.user['hashcode'] = hash_code else: hash_code = self.user['hashcode'] params = dict() params['_'] = fm_utils.timestamp() params['tn'] = 'channellist' params['hashcode'] = hash_code content = self._request(consts.FM_API, extra_params=params) channels = [(c['channel_name'], c['channel_id']) for c in content.json()['channel_list']] return channels def get_fm_play_list(self, channel_id='public_yuzhong_huayu'): """ 根据频道id获取播放列表 :param channel_id: :return: """ params = dict() params['_'] = fm_utils.timestamp() params['tn'] = 'playlist' params['channel_id'] = channel_id params['hashcode'] = '' content = self._request(consts.FM_API, extra_params=params) song_ids = [str(play['id']) for play in content.json()['list']] return song_ids def get_song_info(self, song_ids): """ 根据歌曲IDs获取歌曲信息(歌名,演唱者,歌曲截图...) :param song_ids: """ params = dict() params['songIds'] = ','.join(song_ids) content = self._request(consts.FM_SONG_INFO, 'post', params) return content.json()['data']['songList'] def get_song_link(self, song_ids): params = dict() params['auto'] = 0 params['bat'] = 0 params['bp'] = 0 params['bwt'] = -1 params['dur'] = 211000 params['flag'] = 0 params['hq'] = 1 params['pos'] = 0 params['prerate'] = 128 # 音乐品质(128kbps) params['pt'] = 0 params['rate'] = '' params['s2p'] = -1 params['songIds'] = ','.join(song_ids) params['type'] = 'mp3' content = self._request(consts.FM_SONG_LINK, 'post', params) return content.json()['data']['songList'] def get_next_play_list(self, channel_id, song_id): tt = str(fm_utils.timestamp()) baidu_uid = self.user['BAIDUID'] params = dict() params['_'] = tt url = 'ch_name=%s&item_id=%s&action_no=%d&userid=%d&baiduid=%s' % (channel_id, song_id, 2, 0, baidu_uid) api = consts.FM_NEXT_PLAY_LIST + url content = self._request(api, extra_params=params) song_ids = [str(play['songid']) for play in content.json()['list']] return song_ids def collect(self, song_id): params = dict() params['ids'] = song_id params['type'] = 'song' params['cloud_type'] = 0 content = self._request(consts.FM_COLLECT, 'post', params) data = content.json() error_code = data['errorCode'] or 0 if int(error_code) == 22000: return 1 else: return 0 def del_collect(self, song_id): params = dict() params['songIds'] = song_id params['type'] = 'song' content = self._request(consts.FM_DELETE_COLLECT, 'post', params) data = content.json() error_code = data['errorCode'] or 0 if int(error_code) == 22000: return 1 else: return 0 def iscollect(self, song_id): """ 判断歌曲是否为加❤️, :rtype : object """ params = dict() params['songIds'] = song_id params['type'] = 'type' content = self._request(consts.FM_IS_COLLECT, 'post', params) try: data = int(content.json()) except: data = 0 return data def dislike(self, song_id): """ 加入垃圾箱(不喜欢) :rtype : object """ url = consts.FM_DISLIKE % (song_id, fm_utils.timestamp()) content = self._request(url) data = content.json() error_code = data['errorCode'] or 0 if int(error_code) == 22000: return 1 else: return 0 def get_lrc(self, lrc_url): lrc_dict = dict() if "http" not in lrc_url: lrc_url = "http://fm.baidu.com/" + lrc_url content = self._request(lrc_url).text try: for line in content.split('\n'): line = line.strip() if not line: continue time_stamps = re.findall(r'\[[^\]]+\]', line) value = line times = ''.join(time_stamps) value = value.replace(times, '').strip() for time_s in time_stamps: key = fm_utils.minute_to_s(time_s) if key in lrc_dict: lrc_dict[key] += ' ' + value else: lrc_dict[key] = value except Exception as e: logger.error("get_lrc: %s", str(e.args)) return lrc_dict def is_login(self): """ 判断是否登录 """ user = self.get_fm_user_counts() username = user.get('user_name', '') return username