import requestsimport osimport timeimport refrom difflib import SequenceMatcher################################ !! 用户配置区 - 只需修改这里 !!###############################CONFIG = { # 1. 想下载的歌曲信息 'SONG_NAME': '千年等一回', # 修改为你想下载的歌曲名 'ARTIST_NAME': '鞠婧祎', # 修改为希望的原唱歌手 # 2. 平台搜索优先级 (越靠前越先尝试) 'PLATFORM_PRIORITY': ['netease', 'qqmusic', 'soda'], # 可选: netease, qqmusic, soda # 3. 歌曲名匹配严格度 (0-1, 越高越严格) 'SONG_MATCH_THRESHOLD': 0.6, # 0.6表示至少60%相似度 # 4. 文件保存设置 'SAVE_DIR': './下载的音乐', # 5. 是否启用调试信息 'DEBUG': True}################################ 配置区结束################################ ==================== 通用工具函数 ====================def create_save_dir(path): """创建保存目录""" if not os.path.exists(path): os.makedirs(path) if CONFIG['DEBUG']: print(f"[系统] 创建目录: {path}")def sanitize_filename(filename): """清理文件名中的非法字符""" illegal_chars = r'\/:*?"<>|' for char in illegal_chars: filename = filename.replace(char, '_') filename = filename.replace('\n', '_').replace('\r', '_') if len(filename) > 100: name, ext = os.path.splitext(filename) filename = name[:80] + "..." + ext return filenamedef clean_song_name(song_name): """清理歌曲名,移除括号内容、空格等,用于匹配""" # 移除括号及括号内的内容 song_name = re.sub(r'[\((].*?[\))]', '', song_name) # 移除特殊字符和空格 song_name = re.sub(r'[-\s\.\,]', '', song_name) # 转换为小写(可选) song_name = song_name.lower() return song_namedef calculate_similarity(str1, str2): """计算两个字符串的相似度 (0-1)""" # 先清理两个字符串 str1_clean = clean_song_name(str1) str2_clean = clean_song_name(str2) # 如果清理后完全一致,相似度为1.0 if str1_clean == str2_clean: return 1.0 # 使用difflib计算相似度 return SequenceMatcher(None, str1_clean, str2_clean).ratio()def download_audio(url, filepath, headers): """通用音频下载函数""" try: response = requests.get(url, headers=headers, stream=True, timeout=30) response.raise_for_status() content_type = response.headers.get('Content-Type', '') if not content_type.startswith('audio/') and 'video/' not in content_type: if CONFIG['DEBUG']: print(f"[下载] 警告: 返回内容类型异常: {content_type}") return False with open(filepath, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) return True except Exception as e: if CONFIG['DEBUG']: print(f"[下载] 下载失败: {e}") return False# ==================== 平台搜索模块 ====================class NeteaseMusicSearcher: """网易云音乐搜索器""" @staticmethod def search_song(song_name, artist_name): """在网易云搜索歌曲,返回整理后的结果列表""" print(f"[网易云] 正在搜索: {song_name} - {artist_name}") headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://music.163.com/' } try: # 构造搜索URL encoded_keyword = requests.utils.quote(f"{song_name}{artist_name}") url = f"https://music.163.com/api/search/get/web?csrf_token=&s={encoded_keyword}&type=1&offset=0&limit=20" response = requests.get(url, headers=headers, timeout=10) data = response.json() if data['code'] != 200 or not data.get('result', {}).get('songs'): print("[网易云] 搜索无结果") return [] songs = data['result']['songs'] processed_results = [] target_song_clean = clean_song_name(song_name) for song in songs: song_id = song['id'] song_title = song['name'] primary_artist = song['artists'][0]['name'] if song['artists'] else '未知' # 计算歌曲名相似度 song_similarity = calculate_similarity(song_title, song_name) # 判断是否为原唱 is_original = (artist_name in primary_artist) or (primary_artist in artist_name) # 构造播放链接 play_url = f"https://music.163.com/song/media/outer/url?id={song_id}.mp3" # 计算综合匹配度分数 match_score = 0 # 歌曲名相似度权重: 60% match_score += song_similarity * 60 # 原唱权重: 30% match_score += 30 if is_original else 0 # 歌手名匹配权重: 10% match_score += 10 if (artist_name in primary_artist) else 0 processed_results.append({ 'platform': 'netease', 'id': song_id, 'title': song_title, 'artist': primary_artist, 'is_original': is_original, 'song_similarity': song_similarity, 'match_score': match_score, # 综合匹配分数 'url': play_url, 'source': '网易云音乐' }) # 按匹配度排序 processed_results.sort(key=lambda x: x['match_score'], reverse=True) print(f"[网易云] 找到 {len(processed_results)} 个结果") # 显示前3个最佳匹配 for i, result in enumerate(processed_results[:3]): print(f" {i+1}. {result['title']} - {result['artist']} " f"(相似度: {result['song_similarity']:.2f}, 原唱: {result['is_original']})") return processed_results except Exception as e: print(f"[网易云] 搜索出错: {e}") return []class QQMusicSearcher: """QQ音乐搜索器 (示例框架)""" @staticmethod def search_song(song_name, artist_name): """QQ音乐搜索接口示例""" print(f"[QQ音乐] 正在搜索: {song_name} - {artist_name}") time.sleep(0.5) # 示例数据 - 模拟不同匹配度的结果 example_results = [ { 'platform': 'qqmusic', 'id': '001', 'title': f'{song_name}', 'artist': artist_name, 'is_original': True, 'song_similarity': 0.95, 'match_score': 95, 'url': f'https://api.qq.com/song/001.mp3', 'source': 'QQ音乐' }, { 'platform': 'qqmusic', 'id': '002', 'title': f'{song_name} (Live版)', 'artist': artist_name, 'is_original': True, 'song_similarity': 0.85, 'match_score': 85, 'url': f'https://api.qq.com/song/002.mp3', 'source': 'QQ音乐' }, { 'platform': 'qqmusic', 'id': '003', 'title': f'其他歌曲', 'artist': artist_name, 'is_original': True, 'song_similarity': 0.2, 'match_score': 20, 'url': f'https://api.qq.com/song/003.mp3', 'source': 'QQ音乐' } ] print(f"[QQ音乐] 找到 {len(example_results)} 个结果 (示例)") for i, result in enumerate(example_results[:3]): print(f" {i+1}. {result['title']} - {result['artist']} " f"(相似度: {result['song_similarity']:.2f}, 原唱: {result['is_original']})") return example_resultsclass SodaMusicSearcher: """汽水音乐搜索器 (示例框架)""" @staticmethod def search_song(song_name, artist_name): """汽水音乐搜索接口示例""" print(f"[汽水音乐] 正在搜索: {song_name} - {artist_name}") time.sleep(0.5) example_results = [ { 'platform': 'soda', 'id': 'soda_001', 'title': f'{song_name}', 'artist': '翻唱歌手', 'is_original': False, 'song_similarity': 0.90, 'match_score': 70, # 不是原唱,分数较低 'url': f'https://soda.com/track/soda_001.mp3', 'source': '汽水音乐' } ] print(f"[汽水音乐] 找到 {len(example_results)} 个结果 (示例)") return example_results# ==================== 主逻辑函数 ====================def filter_and_sort_results(results, target_song, target_artist, threshold=0.6): """ 过滤和排序搜索结果 返回: (qualified_results, unqualified_results) """ qualified = [] unqualified = [] for result in results: similarity = result.get('song_similarity', 0) # 如果歌曲名相似度低于阈值,放入不合格列表 if similarity < threshold: unqualified.append(result) else: qualified.append(result) # 对合格的结果按匹配度分数排序 qualified.sort(key=lambda x: x.get('match_score', 0), reverse=True) return qualified, unqualifieddef smart_music_downloader(): """智能多平台音乐下载主函数""" song_name = CONFIG['SONG_NAME'] artist_name = CONFIG['ARTIST_NAME'] platforms = CONFIG['PLATFORM_PRIORITY'] save_dir = CONFIG['SAVE_DIR'] threshold = CONFIG['SONG_MATCH_THRESHOLD'] print("=" * 60) print(f"🎵 智能音乐下载器启动") print(f"📀 目标歌曲: {song_name}") print(f"🎤 目标歌手: {artist_name}") print(f"🎯 歌曲匹配阈值: {threshold}") print(f"🔄 搜索平台: {', '.join(platforms)}") print("=" * 60) create_save_dir(save_dir) searchers = { 'netease': NeteaseMusicSearcher.search_song, 'qqmusic': QQMusicSearcher.search_song, 'soda': SodaMusicSearcher.search_song } # 第一阶段:在所有平台搜索,优先下载高匹配度的原唱版本 print("\n✅ 第一阶段: 搜索并下载最佳匹配版本...") for platform in platforms: if platform not in searchers: print(f"[系统] 跳过未知平台: {platform}") continue print(f"\n{'='*40}") print(f"尝试平台: {platform.upper()}") print(f"{'='*40}") search_func = searchers[platform] results = search_func(song_name, artist_name) if not results: print(f"[{platform}] 无搜索结果,尝试下一平台") continue # 过滤和排序结果 qualified_results, unqualified_results = filter_and_sort_results( results, song_name, artist_name, threshold ) print(f"[{platform}] 合格结果: {len(qualified_results)} 个 | " f"不合格结果: {len(unqualified_results)} 个") if not qualified_results: print(f"[{platform}] 无合格结果(相似度<{threshold}),尝试下一平台") continue # 尝试下载合格结果(已按匹配度排序) for i, song_info in enumerate(qualified_results[:5]): # 最多尝试前5个合格结果 print(f"\n[{platform}] 尝试 #{i+1}: {song_info['title']} - {song_info['artist']}") print(f" 匹配度: {song_info['match_score']}/100 | " f"相似度: {song_info.get('song_similarity', 0):.2f} | " f"原唱: {song_info['is_original']}") # 生成文件名(包含匹配度信息) match_type = "原唱" if song_info['is_original'] else "翻唱" filename = f"{song_info['title']} - {song_info['artist']} ({match_type}_{song_info['source']}_{song_info['match_score']}).mp3" filename = sanitize_filename(filename) filepath = os.path.join(save_dir, filename) # 如果文件已存在,跳过 if os.path.exists(filepath): print(f" 文件已存在,跳过下载") continue headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} success = download_audio(song_info['url'], filepath, headers) if success: print(f"🎉 下载成功! | 平台: {song_info['source']}") print(f"💾 保存路径: {filepath}") print("=" * 60) return True else: print(f"[{platform}] 下载失败,尝试下一版本") # 第二阶段:如果高匹配度版本都失败,尝试放宽条件 print("\n⚠️ 高匹配度版本全部失败,启动备选方案...") print("✅ 第二阶段: 尝试所有可用版本(包括低匹配度)...") for platform in platforms: if platform not in searchers: continue print(f"\n--- 尝试平台: {platform.upper()} (放宽条件) ---") search_func = searchers[platform] results = search_func(song_name, artist_name) if not results: continue # 只按匹配度排序,不进行过滤 results.sort(key=lambda x: x.get('match_score', 0), reverse=True) for i, song_info in enumerate(results[:3]): # 尝试前3个 similarity = song_info.get('song_similarity', 0) print(f"[{platform}] 尝试 #{i+1}: {song_info['title']} - {song_info['artist']} " f"(相似度: {similarity:.2f}, 原唱: {song_info['is_original']})") match_type = "原唱" if song_info['is_original'] else "翻唱" filename = f"{song_info['title']} - {song_info['artist']} ({match_type}_{song_info['source']}_备选).mp3" filename = sanitize_filename(filename) filepath = os.path.join(save_dir, filename) if os.path.exists(filepath): print(f" 文件已存在,跳过下载") continue headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} success = download_audio(song_info['url'], filepath, headers) if success: print(f"🎉 备选版本下载成功! | 平台: {song_info['source']}") print(f"💾 保存路径: {filepath}") print("=" * 60) return True # 所有尝试都失败 print("\n❌ 所有平台和版本尝试均失败") print("\n💡 建议调整策略:") print(f" 1. 降低匹配阈值 (当前: {threshold})") print(f" 2. 检查歌曲名是否正确: '{song_name}'") print(f" 3. 尝试其他平台") print("=" * 60) return False# ==================== 程序入口 ====================if __name__ == "__main__": success = smart_music_downloader() if success: print("✨ 任务完成!") else: print("😔 下载失败") # 等待用户查看结果 if os.name == 'nt': os.system("pause")