From 597603f90e089bc88faf18f2de521e1099f06b23 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Mon, 1 Jun 2026 21:37:41 +0700 Subject: [PATCH] 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 --- src/ui/library_tab.py | 66 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/src/ui/library_tab.py b/src/ui/library_tab.py index 36675f4..444a6c9 100644 --- a/src/ui/library_tab.py +++ b/src/ui/library_tab.py @@ -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()