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 sys
import time import time
import logging import logging
import shutil
from typing import List, Dict, Optional, Tuple, Any from typing import List, Dict, Optional, Tuple, Any
from pathlib import Path from pathlib import Path
@ -753,20 +754,77 @@ class LibraryTab(QWidget):
self._scan_library(force=True) self._scan_library(force=True)
def _on_library_add_files(self): 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( files, _ = QFileDialog.getOpenFileNames(
self, "Select Audio Files", "", source_filter self, "Select Audio Files", "", source_filter
) )
new_files = [] new_source_files = []
copied_ready = []
for f in files: for f in files:
ext = os.path.splitext(f)[1].lower() 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: if ext in SOURCE_EXTS and os.path.exists(f) and f not in self.library_source_files:
self.library_source_files.append(f) 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() 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): def _incremental_update_after_edit(self, file_paths: list):
cache = get_library_cache() cache = get_library_cache()
handler = MetadataHandler() handler = MetadataHandler()