I wanted zip file support as well, so here's a fairly seamless workaround I use. Maybe somebody will find it useful.
I made a python script that will decompress zips (and rars, if rarfile is installed) and then queue the result in musicbee. For easy access I have also added a context menu entry that can be used when right clicking zips to unzip & queue it.
import argparse
import os
import subprocess
import sys
from zipfile import ZipFile
from time import sleep
####################################################################
musicbee_exe = 'C:/Programs/MusicBee/MusicBee.exe'
extract_parent_dir = '~/Temp'
####################################################################
def show_error_dialog(exc_type, exc_value, exc_traceback):
import tkinter as tk
from tkinter import messagebox
import traceback
filename = exc_traceback.tb_frame.f_code.co_filename
error_msg = f"{exc_type.__name__}: {exc_value}\n\n"
error_msg += "\n".join(traceback.format_tb(exc_traceback))
root = tk.Tk()
root.withdraw()
messagebox.showerror(filename, error_msg)
def extract_archive(archive_file, out_dir, skip_single_parent=True):
if archive_file.lower().endswith('.rar'):
from rarfile import RarFile
ArchiveFile = RarFile
skip_single_parent = False
else:
from zipfile import ZipFile
ArchiveFile = ZipFile
with ArchiveFile(archive_file, "r") as archive_file:
names = [n for n in archive_file.namelist() if not n.startswith('__MACOSX')]
subdirs = [os.path.dirname(n) for n in names if '/' in n]
root_dirs = {n.split('/')[0] for n in names if '/' in n}
if skip_single_parent and len(root_dirs) == 1 and len(subdirs) == len(names):
root_dir = list(root_dirs)[0] + '/'
for archive_info in archive_file.infolist():
if archive_info.is_dir() or archive_info.filename.startswith('__MACOSX'):
continue
archive_info.filename = archive_info.filename.replace(root_dir, '', 1)
archive_file.extract(archive_info, out_dir)
else:
for archive_info in archive_file.infolist():
if not archive_info.filename.startswith('__MACOSX'):
archive_file.extract(archive_info, out_dir)
if sys.executable.endswith('pythonw.exe'):
sys.excepthook = show_error_dialog
parser = argparse.ArgumentParser()
parser.add_argument("zip_files", nargs='+', help="path to the zip file(s)")
parser.add_argument("-t", "--track-number", help="queue files in order of the track number in their tags (requires pytaglib)", action="store_true")
parser.add_argument("-a", "--auto", help="enable --track-number automatically if filenames don't start with track number", action="store_true")
parser.add_argument("-i", "--ignore-m3u", help="do not use any m3u files within the archive", action="store_true")
parser.add_argument("-o", "--option", default='QueueLast', help="musicBee option (default: QueueLast), see https://musicbee.fandom.com/wiki/Command_Line_Parameters")
parser.add_argument("-n", "--no-queue", help="do not queue in musicbee, only extract", action="store_true")
parser.add_argument("-p", "--path", default=extract_parent_dir, help=f"parent extract dir (default: {extract_parent_dir})")
args = parser.parse_args()
paths_to_delete = []
extract_parent_dir = os.path.expanduser(args.path)
for zip_file in args.zip_files:
archive_file = os.path.abspath(os.path.expanduser(zip_file))
args.option = '/' + args.option.lstrip('/')
archive_name = os.path.splitext(os.path.basename(archive_file))[0]
temp_dir = os.path.join(extract_parent_dir, archive_name)
os.makedirs(temp_dir, exist_ok=True)
extract_archive(archive_file, temp_dir, skip_single_parent=True)
if args.no_queue:
continue
temp_m3u_name = f'{archive_name}_temp.m3u8'
m3u_file = None
if not args.ignore_m3u:
is_m3u = lambda file: (file.endswith(".m3u") or file.endswith(".m3u8")) and file != temp_m3u_name
m3u_file = next((os.path.join(r, f) for r, d, fnames in os.walk(temp_dir) for f in fnames if is_m3u(f)), None)
if m3u_file:
musicbee_args = [musicbee_exe, args.option, m3u_file.replace('/', '\\')]
subprocess.Popen(musicbee_args)
continue
music_exts_tags = (".mp3", ".flac", ".ogg", ".aac", ".wma", ".m4a", ".alac", ".ape", ".opus", ".mpc")
music_exts_notags = (".wav", ".wave", ".mod", ".mid")
music_exts = music_exts_tags + music_exts_notags
if args.auto:
need_track_num = lambda file: file.lower().endswith(music_exts_tags) and not file.lstrip()[0].isdigit()
args.track_number = any(need_track_num(f) for r, d, fnames in os.walk(temp_dir) for f in fnames)
if args.track_number:
import taglib
files = []
for root, dirs, fnames in os.walk(temp_dir):
for file in fnames:
try:
f = taglib.File(os.path.join(root, file))
key = f.tags.get('DISCNUMBER',['999'])[0].rjust(3,'0') + '-' + f.tags.get('TRACKNUMBER',['999'])[0].rjust(3,'0') + '-' + file
files.append((os.path.join(root, file), key))
except:
if file.lower().endswith(music_exts):
files.append((os.path.join(root, file), f'999-999-{file}'))
files.sort(key=lambda f: f[1])
# Need to create m3u as queueing files one-by-one causes MB to automatically sort by name
temp_m3u = os.path.join(temp_dir, temp_m3u_name)
try:
with open(temp_m3u, 'w', encoding="utf-8") as playlist_file:
for f in files:
playlist_file.write(os.path.relpath(f[0], temp_dir) + '\n')
musicbee_args = [musicbee_exe, args.option, temp_m3u.replace('/', '\\')]
subprocess.Popen(musicbee_args)
finally:
paths_to_delete.append(temp_m3u)
else:
musicbee_args = [musicbee_exe, args.option, temp_dir.replace('/', '\\')]
subprocess.Popen(musicbee_args)
if len(paths_to_delete) > 0:
sleep(10) # wait before deleting in case musicbee hasn't started yet
for temp_m3u in paths_to_delete:
try: os.remove(temp_m3u)
except: pass
Configure the `musicbee_exe` variable at the start to point to the correct location. Then, create a shortcut in "C:\Users\USERNAME\AppData\Roaming\Microsoft\Windows\SendTo". Name it something like "MusicBee Queue Archive". In the file properties, set the target to "python.exe {full path to script}". This is assuming your python.exe is added to PATH. You can also use pythonw.exe instead to hide the command window.
It should now be available in the file explorer as a context menu entry under "Send To". Using it on an archive will extract it to "C:\Users\USERNAME\Temp\{name}" and queue-last the contents in MusicBee.
Sometimes songs in the zip file don't start with the track number (e.g zips from bandcamp), leading to incorrect order of queued files. If you prefer to always queue by the album track number, add ` -a` as an argument in the the properties. This option will sort the tracks by their track number before queuing them, but requires the pytaglib module.