import os import zipfile import json import shutil import hashlib from PIL import Image def calculate_md5_and_size(file_path): md5_hash = hashlib.md5() file_size = 0 with open(file_path, "rb") as file: for byte_block in iter(lambda: file.read(4096), b""): md5_hash.update(byte_block) file_size += len(byte_block) return md5_hash.hexdigest(), file_size def process_folder(folder_path): md5_checksum_dict = {} size_dict = {} for root, dirs, files in os.walk(folder_path): for file_name in files: file_path = os.path.join(root, file_name) relative_path = os.path.relpath(file_path, folder_path) md5_checksum, file_size = calculate_md5_and_size(file_path) md5_checksum_dict[relative_path.replace("\\", "/")] = md5_checksum size_dict[relative_path.replace("\\", "/")] = file_size return md5_checksum_dict, size_dict def write_to_file(file_name, data): with open(file_name, "w") as file: file.write(json.dumps(data, separators=(',', ':'))) def get_unique_song_file(existing_songs, song_file): counter = 1 unique_song_file = song_file while unique_song_file in existing_songs: unique_song_file = f"{song_file}{counter}" counter += 1 return unique_song_file def get_chart_list(dir, manifest): chart_6k = manifest.get("diff_6k").split('/') chart_4k = manifest.get("diff_4k").split('/') dir_list = [] all_chart = chart_6k + chart_4k diff_table = ["_EL.bms", "_EX.bms", "_LPR.bms", "_PR.bms", "_EL4.bms", "_EX4.bms", "_PR4.bms"] for i in range(len(all_chart)): if all_chart[i] != "0": path = os.path.join(dir, "note", manifest.get("mp3") + diff_table[i]) dir_list.append(path) return dir_list def deleteChart(song_id): # Find the song manifest to remove. playmanifest_path = "OverRide/playmanifest.json" with open(playmanifest_path, 'r', encoding="utf-8") as f: playmanifest = json.load(f) manifest_to_remove = None for manifest in playmanifest: if manifest[0].get("id") == song_id: manifest_to_remove = manifest break # We now have the manifest to remove, which is aquired after looping through the main manifest. if manifest_to_remove is None: print("That song ID not found. 没有找到此歌曲ID。") return # Confirm with the user one last time by printing out some useful data. song_title = manifest_to_remove[0].get("title") diff_str = manifest_to_remove[0].get("diff_6k") + "/" + manifest_to_remove[0].get("diff_4k") charter = manifest_to_remove[0].get("charter_6k") + "/" + manifest_to_remove[0].get("charter_4k") print(f"Are you sure you want to delete the song '{song_title}', difficulty '{diff_str}', charted by '{charter}'?") print(f"你确定要删除这首歌吗?名为 '{song_title}',难度 '{diff_str}',谱师为 '{charter}'?") confirm = input("(y/n): ") if confirm.lower() != 'y': print("Deletion cancelled. 删除终止。") return song_name = manifest_to_remove[0].get("mp3") # Remove everything but the chart. paths_to_check = [ (os.path.join("OverRide", "bga", f"{song_name}.zip"), False), (os.path.join("OverRide", "music", f"{song_name}"), False), (os.path.join("Resources", "data_fm", f"{song_name}.jpg"), False), (os.path.join("Resources", "data_fm", f"255{song_name}.jpg"), False), ] for path, _ in paths_to_check: if os.path.exists(path): os.remove(path) # Remove charts charts = get_chart_list(os.path.join("OverRide"), manifest_to_remove[0]) for chart in charts: chart = chart.replace(".bms", "") if os.path.exists(chart): os.remove(chart) # Remove the charts from the zip. note_zip_path = os.path.join("OverRide", "Note.zip") if os.path.exists(note_zip_path): temp_zip_path = os.path.join("OverRide", "temp_Note.zip") with zipfile.ZipFile(note_zip_path, 'r') as note_zip: with zipfile.ZipFile(temp_zip_path, 'w') as temp_zip: for item in note_zip.infolist(): check_name = "offlineNote_" + song_name + "_" file_name = item.filename if not file_name.startswith(check_name): # write file temp_zip.writestr(item, note_zip.read(item.filename)) os.remove(note_zip_path) os.rename(temp_zip_path, note_zip_path) # Remove the manifest, and refresh the metadata. playmanifest.remove(manifest_to_remove) with open(playmanifest_path, 'w', encoding="utf-8") as f: json.dump(playmanifest, f, indent=4) # Correct the main sync and metadata. update_metadata() print("Song has been deleted from the server. 歌曲已从服务器删除。") def importChart(name): # Create a temp directory for unzipping the provided zip file. extract_to = "_temp" if os.path.exists(extract_to): shutil.rmtree(extract_to) os.makedirs(extract_to) if not os.path.isfile(name): print(f"Error: The file '{name}' does not exist.") return # Safely opens the zip file. try: with zipfile.ZipFile(name, 'r') as zip_ref: zip_ref.extractall(extract_to) except zipfile.BadZipFile: print(f"Error: '{name}' is not a valid zip file.") return except PermissionError: print(f"Error: Permission denied. Check your permissions for '{extract_to}'.") return except Exception as e: print(f"An unexpected error occurred when unzipping: {e}") return # Safely read the song manifest manifest_path = os.path.join(extract_to, "manifest.json") if not os.path.isfile(manifest_path): print(f"Error: The file '{manifest_path}' does not exist.") return try: with open(manifest_path, 'r', encoding='utf-8') as f: manifest = json.load(f) except Exception as e: print(f"JSON parse failed for manifest failed: {e}") return for song in manifest: song_file = song.get("mp3") # get list of charts charts = get_chart_list(extract_to, song) # Sanity check everything # (0): at least one chart should be present in the config. standard_check = True reason_fail = "" if charts == []: standard_check = False reason_fail += "\nNo charts found in the chart config this song (all zeroes). " bga_data = song.get("bga") # (1): either the jpg and png version of the thumbnail should exist. img1_path = os.path.join(os.path.join(extract_to, "thumbs"), f"{song_file}.jpg") img2_path = os.path.join(os.path.join(extract_to, "thumbs"), f"{song_file}.png") is_png = False if os.path.exists(img1_path) == False: if os.path.exists(img2_path) == False: standard_check = False reason_fail += "\nSong thumbnail (music_name.jpg or music_name.png) is missing from the thumbs folder. " else: is_png = True # (2): The correct mp3 file should be there. song_path = os.path.join(os.path.join(extract_to, "music"), f"{song_file}.mp3") if os.path.exists(song_path) == False: standard_check = False reason_fail += "\nMusic file (music_name.mp3) is missing from the Music folder. " # (3): If bga data is present, bga file should be there. bga_path = os.path.join(os.path.join(extract_to, "bga"), f"{song_file}.zip") if bga_data != None and os.path.exists(bga_path) == False: standard_check = False reason_fail += "\nSince manifest specified BGA, there should be a music_name.zip in the bga folder. " # (4): If bga data is not present but there is a song_name.zip in bga folder. if bga_data == None and os.path.exists(bga_path): standard_check = False reason_fail += "\nManifest did not specify BGA, but there is a music_name.zip in the bga folder. While this is not a breaking issue, if you wish to use the BGA please specify it in the manifest. " # (5): All the charts specified in the manifest should exist for chart in charts: if os.path.exists(chart) == False: standard_check = False reason_fail += "\nA chart that is specified in manifest is missing from the note folder: " + chart + ". " # That is it! if standard_check == False: print("The song contains issues and is not imported. 此曲目存在问题,没有导入。") print("The reasons are as follows. 原因如下。") print(reason_fail) shutil.rmtree(extract_to) continue playmanifest_path = "OverRide/playmanifest.json" with open(playmanifest_path, 'r', encoding="utf-8") as f: playmanifest = json.load(f) existing_song_files = [item[0].get("mp3") for item in playmanifest] unique_song_file = get_unique_song_file(existing_song_files, song_file) # Copy bga zip file if bga is specified if bga_data != None: new_bga_name = unique_song_file + ".zip" new_bga_path = os.path.join("OverRide", "bga", new_bga_name) os.makedirs(os.path.dirname(new_bga_path), exist_ok=True) shutil.move(bga_path, new_bga_path) # Copy music file new_song_name = unique_song_file new_song_path = os.path.join("OverRide", "Music", new_song_name) os.makedirs(os.path.dirname(new_song_path), exist_ok=True) shutil.move(song_path, new_song_path) # Copy the thumbnails new_img_name = unique_song_file + ".jpg" new_img_path = os.path.join("Resources", "data_fm", new_img_name) os.makedirs(os.path.dirname(new_img_path), exist_ok=True) if is_png: with Image.open(img2_path) as img: rgb_img = img.convert("RGB") rgb_img.save(new_img_path, "JPEG") else: shutil.move(img1_path, new_img_path) # Save a resized 255x255 version of the image resized_img_name = "255" + unique_song_file + ".jpg" resized_img_path = os.path.join("Resources", "data_fm", resized_img_name) with Image.open(new_img_path) as img: img = img.resize((255, 255), Image.Resampling.LANCZOS) img.save(resized_img_path, "JPEG") # Copy all the charts for chart in charts: new_chart_name = os.path.basename(chart).replace(".bms", "") new_chart_name = new_chart_name.replace(song_file, unique_song_file) new_chart_path = os.path.join("OverRide", "Note", new_chart_name) os.makedirs(os.path.dirname(new_chart_path), exist_ok=True) shutil.copy(chart, new_chart_path) # Add charts to zip note_zip_path = os.path.join("OverRide", "Note.zip") new_charts = [] for chart in charts: new_chart_name = chart.replace(song_file, f"offlineNote_{unique_song_file}") new_chart_name = new_chart_name.replace(".bms", "") os.rename(chart, new_chart_name) new_charts.append(new_chart_name) with zipfile.ZipFile(note_zip_path, 'a') as note_zip: for chart in new_charts: note_zip.write(chart, arcname=os.path.basename(chart)) # Merge the manifest if playmanifest: last_id = playmanifest[-1][0].get("id", 0) else: last_id = 0 song["id"] = last_id + 1 song["mp3"] = unique_song_file playmanifest.append([song]) with open(playmanifest_path, 'w', encoding="utf-8") as f: json.dump(playmanifest, f, indent=4) print(f"Song added: 歌曲已添加:{unique_song_file}") # Refresh the metadata and we are done! update_metadata() shutil.rmtree(extract_to) def update_metadata(): resources_folder = "Resources" md5_checksum_dict, size_dict = process_folder(resources_folder) md5_file_name = "OverRapid/Md5ListChecksum.json" size_file_name = "OverRapid/SizeList.json" write_to_file(md5_file_name, md5_checksum_dict) write_to_file(size_file_name, size_dict) print(f"MD5 and size checksums saved.") def main(): print("Override Concierge - 您的Override管家") print("For music import, simply enter the zip file's path. For music deletion, simply enter its SID as displayed in-game.") print("曲目导入请输入zip文件的目录。曲目删除请输入游戏内显示的SID。") user_input = input("Input 输入: ") if user_input.isdigit(): deleteChart(int(user_input)) elif not user_input.endswith(".zip"): print("The file is not zip. 文件不是zip。") else: importChart(user_input) if __name__ == "__main__": main()