feat: add ready audio files (m4a/mp3/aac) to library with structured copy

- Expand Add Files dialog to accept all supported audio formats
- Copy ready files to output_dir/Artist/Album/{track} - Title.ext
- Fallback to output_dir root when metadata is missing
- Handle filename collisions with numeric suffix
This commit is contained in:
Maksim Totmin 2026-06-01 21:37:41 +07:00
parent b7367e63c4
commit 597603f90e

View File

@ -8,6 +8,7 @@ import os
import sys
import time
import logging
import shutil
from typing import List, Dict, Optional, Tuple, Any
from pathlib import Path
@ -753,20 +754,77 @@ class LibraryTab(QWidget):
self._scan_library(force=True)
def _on_library_add_files(self):
source_filter = "Audio Files (*.flac *.wav *.ogg *.opus *.wma *.ape *.aiff *.alac)"
all_exts = sorted(SOURCE_EXTS | AUDIO_EXTS)
source_filter = f"Audio Files (*{' *'.join(all_exts)})"
files, _ = QFileDialog.getOpenFileNames(
self, "Select Audio Files", "", source_filter
)
new_files = []
new_source_files = []
copied_ready = []
for f in files:
ext = os.path.splitext(f)[1].lower()
if ext in SOURCE_EXTS and os.path.exists(f) and f not in self.library_source_files:
self.library_source_files.append(f)
new_files.append(f)
new_source_files.append(f)
elif ext in AUDIO_EXTS and os.path.exists(f):
output_dir = self.get_output_dir()
if output_dir:
dest = self._copy_to_library(f, output_dir)
if dest:
copied_ready.append(dest)
if new_files:
if new_source_files or copied_ready:
self._scan_library()
def _copy_to_library(self, src: str, output_dir: str) -> str:
handler = MetadataHandler()
ext = os.path.splitext(src)[1].lower()
try:
info = handler.extract_tags(src)
artist = info.artist if info and info.artist else None
album = info.album if info and info.album else None
title = info.title if info and info.title else None
track_num = info.track_number if info else 0
except Exception:
artist = album = title = None
track_num = 0
if title:
safe_title = self._sanitize_filename(title)
safe_artist = self._sanitize_filename(artist) if artist else "_Unknown"
safe_album = self._sanitize_filename(album) if album else "_Unknown"
fname = f"{track_num:02d} - {safe_title}{ext}" if track_num > 0 else f"{safe_title}{ext}"
dest_dir = os.path.join(output_dir, safe_artist, safe_album)
else:
fname = os.path.basename(src)
dest_dir = output_dir
os.makedirs(dest_dir, exist_ok=True)
dest = os.path.join(dest_dir, fname)
if os.path.normpath(src) == os.path.normpath(dest):
return dest
if not os.path.exists(dest):
shutil.copy2(src, dest)
return dest
base, ext = os.path.splitext(fname)
for i in range(1, 100):
dest = os.path.join(dest_dir, f"{base}_{i}{ext}")
if not os.path.exists(dest):
shutil.copy2(src, dest)
return dest
return None
@staticmethod
def _sanitize_filename(filename: str) -> str:
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '_')
if len(filename) > 100:
filename = filename[:97] + '...'
return filename.strip()
def _incremental_update_after_edit(self, file_paths: list):
cache = get_library_cache()
handler = MetadataHandler()