USER
verbesser das gui dieser anwendung. sorge zudem auch dafür das alles auf einem tab ist damit die vorgänge und resultate auf einem blick sichtbar sind
überdenke nocheinmal die gesamte umsetzung des gui und erstelle es so das es optimal und sinnvoll für diese anwendung ist. Es sollte vorallem übersichtlich, modern, flexibel, intuitiv, informativ, komfortable und nutzerfreundlich sein. nenne mir dann den vollständigen code der gesamten anwendung.
import os
import sys
import logging
import traceback
import warnings
import re
import configparser
from datetime import datetime
from typing import List, Tuple, Optional
import time
from PyQt5.QtWidgets import (
QTabWidget, QGridLayout, QApplication, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout,
QLineEdit, QFileDialog, QLabel, QWidget, QProgressBar, QTableWidget, QTableWidgetItem,
QHeaderView, QCheckBox, QTextEdit, QMessageBox, QSlider, QSpinBox, QFormLayout,
QDialog, QAction, QStyle, QGroupBox, QComboBox, QSizePolicy, QSpacerItem
)
from PyQt5.QtCore import (
Qt, QTimer, QObject, pyqtSignal, QThread, QThreadPool, QRunnable,
QIODevice, QBuffer, QUrl
)
from PyQt5.QtGui import QPixmap, QIcon, QPalette, QColor, QTextCursor
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from pydub import AudioSegment
from pydub.silence import split_on_silence
import requests
from mutagen.id3 import ID3, TIT2, TPE1, TALB, APIC, error
from mutagen.mp3 import MP3
from yt_dlp import YoutubeDL
from functools import partial
import tempfile
import threading
# Unterdrücke Deprecation-Warnings von PyQt5
warnings.filterwarnings('ignore', category=DeprecationWarning)
# Optional: MusicBrainz für die Metadatenabfrage (kostenlose Nutzung, kein API-Schlüssel erforderlich)
try:
import musicbrainzngs
musicbrainzngs.set_useragent("MusicIdentifierApp", "1.0", "your-email@example.com")
MUSICBRAINZ_AVAILABLE = True
except ImportError:
MUSICBRAINZ_AVAILABLE = False
class MetadataFetcher(QObject):
"""Klasse zur asynchronen Abrufung von Metadaten für einen gegebenen Musiktitel."""
result_ready = pyqtSignal(int, str, str, str, object)
progress = pyqtSignal(int)
def __init__(self, index: int, title: str, total: int):
super().__init__()
self.index = index
self.title = title
self.total = total
self.cache = {}
def run(self):
"""Startet den Prozess zum Abrufen der Metadaten."""
try:
logging.info(f"Starte Metadatenabfrage für Index {self.index}: '{self.title}'")
cover_data, artist, album, track_title = self.get_cover_and_metadata(self.title)
self.result_ready.emit(self.index, artist, album, track_title, cover_data)
except Exception as e:
logging.error(f"Fehler beim Abrufen der Metadaten für Index {self.index}: {str(e)}")
self.result_ready.emit(self.index, '', '', self.title, None)
finally:
progress_value = int(((self.index + 1) / self.total) * 100)
self.progress.emit(progress_value)
def get_cover_and_metadata(self, title: str) -> Tuple[Optional[bytes], str, str, str]:
"""Versucht, Metadaten anhand des Titels zu erhalten."""
if title in self.cache:
logging.info(f"Metadaten aus Cache für '{title}' abgerufen.")
return self.cache[title]
# Verwende MusicBrainz, falls verfügbar
if MUSICBRAINZ_AVAILABLE:
logging.info(f"Verwende MusicBrainz für die Identifizierung von '{title}'")
artist, album, track_title, cover_data = self.search_musicbrainz(title)
if artist and track_title:
self.cache[title] = (cover_data, artist, album, track_title)
return cover_data, artist, album, track_title
# Fallback auf iTunes-Suche
logging.info(f"Führe iTunes-Suche für '{title}' durch.")
cover_url, artist, album, track_title = self.search_itunes(title)
if artist and track_title:
cover_data = self.download_cover(cover_url)
self.cache[title] = (cover_data, artist, album, track_title)
return cover_data, artist, album, track_title
# Wenn alles fehlschlägt
logging.warning(f"Keine Metadaten für '{title}' gefunden.")
return None, '', '', title
def search_musicbrainz(self, query: str) -> Tuple[str, str, str, Optional[bytes]]:
"""Sucht Metadaten in der MusicBrainz-Datenbank."""
try:
result = musicbrainzngs.search_recordings(query=query, limit=1)
if result['recording-list']:
recording = result['recording-list'][0]
artist = recording['artist-credit'][0]['name']
track_title = recording['title']
# Album abfragen
if 'release-list' in recording and recording['release-list']:
album = recording['release-list'][0]['title']
# Cover abfragen
cover_data = self.get_cover_from_coverartarchive(recording['release-list'][0]['id'])
else:
album = ''
cover_data = None
return artist, album, track_title, cover_data
except Exception as e:
logging.error(f"Fehler bei der MusicBrainz-Abfrage: {e}")
return '', '', '', None
def get_cover_from_coverartarchive(self, release_id: str) -> Optional[bytes]:
"""Lädt das Cover von der Cover Art Archive API herunter."""
url = f"https://coverartarchive.org/release/{release_id}/front"
try:
response = requests.get(url)
if response.status_code == 200 and response.headers['Content-Type'].startswith('image'):
return response.content
except Exception as e:
logging.error(f"Fehler beim Abrufen des Covers von Cover Art Archive: {e}")
return None
def search_itunes(self, query: str) -> Tuple[Optional[str], str, str, str]:
"""Sucht Metadaten in der iTunes API."""
url = 'https://itunes.apple.com/search'
params = {
'term': query,
'media': 'music',
'entity': 'song',
'limit': 1
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
results = response.json().get('results')
if results:
info = results[0]
cover_url = info.get('artworkUrl100', '').replace('100x100bb.jpg', '600x600bb.jpg')
artist = info.get('artistName', '')
album = info.get('collectionName', '')
track_title = info.get('trackName', '')
return cover_url, artist, album, track_title
except Exception as e:
logging.error(f"Fehler bei der iTunes-Abfrage: {e}")
return None, '', '', ''
def download_cover(self, cover_url: Optional[str]) -> Optional[bytes]:
"""Lädt das Albumcover herunter."""
if cover_url:
try:
response = requests.get(cover_url)
if response.status_code == 200 and response.headers['Content-Type'].startswith('image'):
return response.content
except Exception as e:
logging.error(f"Fehler beim Herunterladen des Covers: {e}")
return None
class MetadataFetcherTask(QRunnable):
def __init__(self, fetcher):
super().__init__()
self.fetcher = fetcher
def run(self):
self.fetcher.run()
class SaveWorker(QObject):
"""Arbeiterklasse für den Speichervorgang."""
progress = pyqtSignal(int)
finished = pyqtSignal()
def __init__(self, tracks_to_save):
super().__init__()
self.tracks_to_save = tracks_to_save
def run(self):
total = len(self.tracks_to_save)
for i, index in enumerate(self.tracks_to_save):
self.tracks_to_save[index]['save_function']()
progress = int((i + 1) / total * 100)
self.progress.emit(progress)
self.finished.emit()
class Worker(QObject):
"""
Arbeiterklasse für die Verarbeitung von Audio, einschließlich Download, Extraktion, Segmentierung und Identifizierung.
"""
segmentation_finished = pyqtSignal()
finished = pyqtSignal()
progress = pyqtSignal(int, str)
error = pyqtSignal(str)
segments_ready = pyqtSignal(list, list)
download_progress = pyqtSignal(int)
extraction_progress = pyqtSignal(int)
segmentation_progress = pyqtSignal(int)
overall_progress = pyqtSignal(int)
def __init__(self, url_or_path: str, settings: dict):
super().__init__()
self.url_or_path = url_or_path
self.settings = settings
self.segments: List[AudioSegment] = []
self.segments_info: List[dict] = []
self.video_description = ''
self.audio: Optional[AudioSegment] = None
self.manual_timestamps = []
self.total_steps = 3 # Download, Extraktion, Segmentierung
def run(self):
"""Startet den Verarbeitungsprozess."""
try:
self.progress.emit(0, "Audio wird vorbereitet...")
self.overall_progress.emit(0)
step = 0
if self.url_or_path.lower().startswith('http'):
success = self.download_audio(self.url_or_path)
if not success:
self.error.emit("Download fehlgeschlagen.")
return
step += 1
self.overall_progress.emit(int((step / self.total_steps) * 100))
else:
self.process_local_file(self.url_or_path)
step += 1
self.overall_progress.emit(int((step / self.total_steps) * 100))
self.segment_and_identify()
step += 1
self.overall_progress.emit(int((step / self.total_steps) * 100))
self.finished.emit()
except Exception as e:
error_message = f"Ein Fehler ist aufgetreten: {str(e)}"
logging.error(error_message)
logging.error(traceback.format_exc())
self.error.emit(error_message)
self.finished.emit()
def download_audio(self, url: str) -> bool:
"""Lädt das Audio von der gegebenen URL herunter."""
self.progress.emit(10, "Audio wird heruntergeladen...")
self.download_progress.emit(0)
success = False
try:
temp_dir = tempfile.mkdtemp()
output_template = os.path.join(temp_dir, 'downloaded_audio.%(ext)s')
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': output_template,
'quiet': True,
'no_warnings': True,
'ignoreerrors': True,
'nocheckcertificate': True,
'socket_timeout': 30,
'progress_hooks': [self.ydl_hook],
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320',
}],
'paths': {'home': temp_dir}
}
with YoutubeDL(ydl_opts) as ydl:
info_dict = ydl.extract_info(url, download=True)
if info_dict is not None:
self.video_description = info_dict.get('description', '')
filename = ydl.prepare_filename(info_dict)
filename = os.path.splitext(filename)[0] + '.mp3'
if os.path.exists(filename):
self.extract_audio(filename)
os.remove(filename)
success = True
logging.info("Download mit yt_dlp erfolgreich.")
else:
logging.warning("Heruntergeladene Datei nicht gefunden.")
self.error.emit("Heruntergeladene Datei nicht gefunden.")
else:
logging.warning("Download mit yt_dlp fehlgeschlagen.")
self.error.emit("Download fehlgeschlagen.")
except Exception as e_yt_dlp:
logging.error(f"Download mit yt_dlp fehlgeschlagen: {e_yt_dlp}")
self.error.emit(f"Download fehlgeschlagen: {e_yt_dlp}")
finally:
self.progress.emit(30, "Download abgeschlossen.")
return success
def ydl_hook(self, d):
if d['status'] == 'downloading':
if d.get('total_bytes'):
percentage = d.get('downloaded_bytes', 0) / d.get('total_bytes', 1) * 100
elif d.get('total_bytes_estimate'):
percentage = d.get('downloaded_bytes', 0) / d.get('total_bytes_estimate', 1) * 100
else:
percentage = 0
self.download_progress.emit(int(percentage))
elif d['status'] == 'finished':
self.download_progress.emit(100)
def process_local_file(self, file_path: str):
"""Verarbeitet eine lokale Datei."""
self.progress.emit(10, "Lokale Datei wird verarbeitet...")
self.extract_metadata_from_file(file_path)
self.extract_audio(file_path)
def extract_audio(self, file_path: str):
"""Extrahiert das Audio aus der gegebenen Datei."""
try:
if not os.path.exists(file_path):
raise Exception(f"Audiodatei nicht gefunden: {file_path}")
self.progress.emit(35, "Audio wird extrahiert...")
self.extraction_progress.emit(0)
def update_progress():
for i in range(0, 101, 10):
time.sleep(0.05)
self.extraction_progress.emit(i)
threading.Thread(target=update_progress).start()
self.audio = AudioSegment.from_file(file_path)
self.extraction_progress.emit(100)
self.progress.emit(40, "Audio erfolgreich extrahiert.")
logging.info("Audio erfolgreich extrahiert.")
except Exception as e:
raise Exception(f"Fehler beim Extrahieren des Audios: {str(e)}")
def extract_metadata_from_file(self, file_path: str):
"""Extrahiert Metadaten aus der lokalen Datei."""
try:
if file_path.lower().endswith(('.mp3', '.wav', '.flac', '.ogg')):
audio = MP3(file_path)
title = audio.tags.get("TIT2", "")
artist = audio.tags.get("TPE1", "")
self.video_description = f"{title} {artist}"
logging.info("Metadaten aus lokaler Datei extrahiert.")
except (AttributeError, error) as e:
logging.warning(f"Keine Metadaten aus lokaler Datei extrahiert: {e}")
def segment_and_identify(self):
"""Segmentiert das Audio und identifiziert die einzelnen Tracks."""
self.progress.emit(45, "Audio wird segmentiert...")
self.segmentation_progress.emit(0)
timestamps = self.parse_timestamps(self.video_description)
if timestamps:
logging.info("Zeitstempel aus Videobeschreibung werden für die Segmentierung verwendet.")
self.segment_using_timestamps(timestamps)
else:
logging.info("Keine Zeitstempel gefunden.")
if self.manual_timestamps:
logging.info("Verwende manuell eingegebene Zeitstempel.")
self.segment_using_timestamps(self.manual_timestamps)
else:
logging.info("Versuche Stilleerkennung.")
self.segment_using_silence_detection()
if self.segments:
logging.info(f"{len(self.segments)} Segmente durch Stilleerkennung gefunden.")
else:
logging.info("Stilleerkennung fehlgeschlagen, versuche alternative Segmentierung.")
self.segment_using_fixed_length()
if not self.segments:
logging.warning("Alternative Segmentierung fehlgeschlagen, gesamtes Audio wird verwendet.")
self.segments.append(self.audio)
self.segments_info.append({'title': 'Gesamtes Audio'})
self.segmentation_progress.emit(100)
self.progress.emit(80, "Segmentierung abgeschlossen.")
self.segments_ready.emit(self.segments_info, self.segments)
def segment_using_timestamps(self, timestamps: List[Tuple[int, str]]):
"""Segmentiert das Audio basierend auf Zeitstempeln."""
total = len(timestamps)
for i, (start_time, title) in enumerate(timestamps):
progress = int(((i + 1) / total) * 100)
self.segmentation_progress.emit(progress)
start_ms = int(start_time * 1000)
end_ms = int(timestamps[i + 1][0] * 1000) if i + 1 < len(timestamps) else len(self.audio)
segment = self.audio[start_ms:end_ms]
self.segments.append(segment)
self.segments_info.append({'title': title})
logging.info(f"Segment {i + 1} mit Titel '{title}' erstellt.")
def segment_using_silence_detection(self):
"""Segmentiert das Audio durch Stilleerkennung."""
if not self.audio:
logging.error("Keine Audiodaten zum Segmentieren vorhanden.")
return
min_silence_len = self.settings.get('min_silence_len', 2000)
silence_thresh_adjust = self.settings.get('silence_thresh_adjust', -16)
silence_thresh = self.audio.dBFS + silence_thresh_adjust
keep_silence = self.settings.get('keep_silence', 500)
logging.info(f"Stilleerkennung mit min_silence_len={min_silence_len}, silence_thresh={silence_thresh}")
chunks = split_on_silence(
self.audio,
min_silence_len=min_silence_len,
silence_thresh=silence_thresh,
keep_silence=keep_silence
)
min_segment_length = self.settings.get('min_segment_length', 10000)
segments = [s for s in chunks if len(s) > min_segment_length]
if segments:
self.segments = segments
self.segments_info = [{'title': f'Track {i + 1}'} for i in range(len(segments))]
else:
logging.warning("Keine Segmente mit Stilleerkennung gefunden.")
def segment_using_fixed_length(self, segment_length_ms: int = 180000):
"""Segmentiert das Audio in feste Längen."""
if not self.audio:
logging.error("Keine Audiodaten zum Segmentieren vorhanden.")
return
self.segments = []
self.segments_info = []
total_length_ms = len(self.audio)
num_segments = total_length_ms // segment_length_ms + 1
for i, start_ms in enumerate(range(0, total_length_ms, segment_length_ms)):
progress = int(((i + 1) / num_segments) * 100)
self.segmentation_progress.emit(progress)
end_ms = min(start_ms + segment_length_ms, total_length_ms)
segment = self.audio[start_ms:end_ms]
self.segments.append(segment)
self.segments_info.append({'title': f'Track {len(self.segments)}'})
logging.info(f"{len(self.segments)} Segmente durch feste Länge erzeugt.")
def parse_timestamps(self, description: str) -> List[Tuple[int, str]]:
"""Parst Zeitstempel und Titel aus der Videobeschreibung."""
timestamps = []
lines = description.splitlines()
for line in lines:
line = line.strip()
if not line:
continue
# Muster für verschiedene Zeitstempelformate
patterns = [
r'^(\d{1,2}:\d{1,2}:\d{1,2})\s*[-–—]\s*(.+)$',
r'^(\d{1,2}:\d{1,2}:\d{1,2})\s+(.+)$',
r'^(\d{1,2}:\d{1,2})\s*[-–—]\s*(.+)$',
r'^(\d{1,2}:\d{1,2})\s+(.+)$',
r'^(\d{1,2}\.\d{1,2}\.\d{1,2})\s*[-–—]?\s*(.+)$',
r'^(\d{1,2}\.\d{1,2})\s*[-–—]?\s*(.+)$',
r'^(\d+)\s*[-–—]?\s*(.+)$',
]
matched = False
for pattern in patterns:
match = re.match(pattern, line)
if match:
time_str = match.group(1)
title = match.group(2)
total_seconds = self.convert_time_to_seconds(time_str)
if total_seconds is not None:
timestamps.append((total_seconds, title.strip()))
matched = True
break
if not matched:
logging.debug(f"Kein gültiger Zeitstempel in Zeile: {line}")
timestamps.sort(key=lambda x: x[0])
return timestamps
def convert_time_to_seconds(self, time_str: str) -> Optional[int]:
"""Konvertiert einen Zeitstring in Sekunden."""
try:
time_str = time_str.strip()
if ':' in time_str:
parts = [int(p) for p in time_str.split(':')]
if len(parts) == 3:
hours, minutes, seconds = parts
elif len(parts) == 2:
hours = 0
minutes, seconds = parts
else:
return None
total_seconds = hours * 3600 + minutes * 60 + seconds
elif '.' in time_str:
parts = [int(p) for p in time_str.split('.')]
if len(parts) == 3:
hours, minutes, seconds = parts
elif len(parts) == 2:
hours = 0
minutes, seconds = parts
else:
return None
total_seconds = hours * 3600 + minutes * 60 + seconds
else:
total_seconds = int(time_str)
return total_seconds
except ValueError:
return None
class QTextEditLogger(logging.Handler):
"""Logging Handler, der Logs in ein QTextEdit schreibt."""
def __init__(self, text_edit):
super().__init__()
self.widget = text_edit
def emit(self, record):
msg = self.format(record)
self.widget.append(msg)
class TimestampsDialog(QDialog):
"""Dialog zur manuellen Eingabe von Zeitstempeln."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Zeitstempel eingeben")
self.layout = QVBoxLayout()
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText(
"Geben Sie hier die Zeitstempel und Titel ein.\nZum Beispiel:\n00:00 Titel 1\n03:45 Titel 2\n..."
)
self.layout.addWidget(self.text_edit)
button_layout = QHBoxLayout()
ok_button = QPushButton("OK")
ok_button.clicked.connect(self.accept)
cancel_button = QPushButton("Abbrechen")
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
self.layout.addLayout(button_layout)
self.setLayout(self.layout)
def get_timestamps(self):
return self.text_edit.toPlainText()
class SettingsDialog(QDialog):
"""Dialog zur Anzeige und Änderung der Anwendungseinstellungen."""
def __init__(self, parent=None, settings=None):
super().__init__(parent)
self.setWindowTitle("Einstellungen")
self.settings = settings or {}
self.layout = QVBoxLayout()
form_layout = QFormLayout()
self.min_silence_len_spinbox = QSpinBox()
self.min_silence_len_spinbox.setRange(500, 10000)
self.min_silence_len_spinbox.setValue(self.settings.get('min_silence_len', 2000))
self.min_silence_len_spinbox.setToolTip(
"Gibt die minimale Länge der Stille (in Millisekunden) an, um als Trennung zwischen Tracks zu gelten."
)
form_layout.addRow("Minimale Stillelänge (ms):", self.min_silence_len_spinbox)
self.silence_thresh_spinbox = QSpinBox()
self.silence_thresh_spinbox.setRange(-60, 0)
self.silence_thresh_spinbox.setValue(self.settings.get('silence_thresh_adjust', -16))
self.silence_thresh_spinbox.setToolTip(
"Gibt den Schwellenwert (in dB) an, unterhalb dessen Audio als Stille betrachtet wird."
)
form_layout.addRow("Stille-Schwellenwert (dB):", self.silence_thresh_spinbox)
self.keep_silence_spinbox = QSpinBox()
self.keep_silence_spinbox.setRange(0, 5000)
self.keep_silence_spinbox.setValue(self.settings.get('keep_silence', 500))
self.keep_silence_spinbox.setToolTip(
"Gibt die Länge der Stille (in Millisekunden) an, die am Anfang und Ende jedes Segments beibehalten wird."
)
form_layout.addRow("Stille beibehalten (ms):", self.keep_silence_spinbox)
self.default_save_checkbox = QCheckBox()
default_save = self.settings.get('default_save_all', True)
self.default_save_checkbox.setChecked(default_save)
self.default_save_checkbox.setToolTip("Legt fest, ob alle Tracks standardmäßig zum Speichern ausgewählt sind.")
form_layout.addRow("Alle Tracks standardmäßig auswählen:", self.default_save_checkbox)
# Themenauswahl
self.theme_combo = QComboBox()
self.theme_combo.addItems(["Dunkel", "Hell"])
current_theme = self.settings.get('theme', 'Dunkel')
index = self.theme_combo.findText(current_theme)
if index != -1:
self.theme_combo.setCurrentIndex(index)
self.theme_combo.setToolTip("Wählen Sie das Farbschema der Anwendung.")
form_layout.addRow("Thema:", self.theme_combo)
self.layout.addLayout(form_layout)
button_layout = QHBoxLayout()
ok_button = QPushButton("OK")
ok_button.clicked.connect(self.accept)
cancel_button = QPushButton("Abbrechen")
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
self.layout.addLayout(button_layout)
self.setLayout(self.layout)
def get_settings(self):
return {
'min_silence_len': self.min_silence_len_spinbox.value(),
'silence_thresh_adjust': self.silence_thresh_spinbox.value(),
'keep_silence': self.keep_silence_spinbox.value(),
'default_save_all': self.default_save_checkbox.isChecked(),
'theme': self.theme_combo.currentText()
}
class MusicIdentifierApp(QMainWindow):
"""Hauptanwendungsklasse für den Musik-Identifikator."""
def __init__(self):
super().__init__()
self.setWindowTitle("Music Identifier")
self.resize(1200, 800) # Erhöhte Größe des Hauptfensters
self.setWindowIcon(QIcon.fromTheme("audio-x-generic"))
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("application.log"),
logging.StreamHandler()
]
)
self.config = configparser.ConfigParser()
self.load_settings()
self.output_dir = self.config.get('SETTINGS', 'output_dir', fallback=os.getcwd())
self.init_ui()
self.segments = []
self.segments_info = []
self.total_segments = 0
self.fetchers = {}
self.thread_pool = QThreadPool()
self.players = {} # Dictionary to store media players for each track
self.is_processing = False # Flag to indicate if processing is ongoing
def init_ui(self):
"""Initialisiert die Benutzeroberfläche."""
self.apply_theme()
# Haupt-Tab-Widget
self.tabs = QTabWidget()
self.setCentralWidget(self.tabs)
# Registerkarte: Audioquelle und Start
self.tab_input = QWidget()
self.tabs.addTab(self.tab_input, "Audioquelle")
self.init_tab_input()
# Registerkarte: Fortschritt und Informationen
self.tab_progress = QWidget()
self.tabs.addTab(self.tab_progress, "Fortschritt")
self.init_tab_progress()
# Registerkarte: Erkannte Musikstücke
self.tab_music = QWidget()
self.tabs.addTab(self.tab_music, "Erkannte Musikstücke")
self.init_tab_music()
# Registerkarte: Protokoll
self.tab_log = QWidget()
self.tabs.addTab(self.tab_log, "Protokoll")
self.init_tab_log()
# Registerkarte: Einstellungen
self.tab_settings = QWidget()
self.tabs.addTab(self.tab_settings, "Einstellungen")
self.init_tab_settings()
# Menüleiste
self.init_menu()
# Statusleiste
self.status_bar = self.statusBar()
self.status_bar.showMessage("Bereit")
def init_menu(self):
"""Initialisiert die Menüleiste."""
menubar = self.menuBar()
file_menu = menubar.addMenu('Datei')
settings_action = QAction('Einstellungen', self)
settings_action.triggered.connect(self.open_settings_dialog)
exit_action = QAction('Beenden', self)
exit_action.triggered.connect(self.close)
file_menu.addAction(settings_action)
file_menu.addSeparator()
file_menu.addAction(exit_action)
help_menu = menubar.addMenu('Hilfe')
about_action = QAction('Über', self)
about_action.triggered.connect(self.show_about_dialog)
help_menu.addAction(about_action)
def init_tab_input(self):
"""Initialisiert die Registerkarte für die Audioquelle."""
layout = QVBoxLayout()
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
# Eingabe-Layout
input_layout = QHBoxLayout()
input_layout.setSpacing(10)
self.url_input = QLineEdit(self)
self.url_input.setPlaceholderText("Geben Sie eine URL ein oder wählen Sie eine lokale Datei aus")
self.url_input.setToolTip("Geben Sie eine URL (z.B. YouTube-Link) oder den Pfad zu einer lokalen Datei ein.")
self.url_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
input_layout.addWidget(self.url_input)
self.select_file_btn = QPushButton("Datei auswählen")
self.select_file_btn.setIcon(QIcon.fromTheme("document-open"))
self.select_file_btn.setToolTip("Öffnen Sie einen Dialog, um eine lokale Audio-/Videodatei auszuwählen.")
self.select_file_btn.setFixedWidth(150)
self.select_file_btn.clicked.connect(self.open_file_dialog)
input_layout.addWidget(self.select_file_btn)
layout.addLayout(input_layout)
# Start-Button
self.start_btn = QPushButton("Start")
self.start_btn.setIcon(QIcon.fromTheme("media-playback-start"))
self.start_btn.setToolTip("Starten Sie die Verarbeitung des eingegebenen Links oder der ausgewählten Datei.")
self.start_btn.setFixedHeight(50)
self.start_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-size: 18px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
self.start_btn.clicked.connect(self.handle_start)
layout.addWidget(self.start_btn)
# Spacer
layout.addStretch()
self.tab_input.setLayout(layout)
def init_tab_progress(self):
"""Initialisiert die Registerkarte für den Fortschritt."""
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
self.current_action_label = QLabel("Bereit")
self.current_action_label.setStyleSheet("font-size: 16px; padding: 10px; font-weight: bold;")
self.overall_progress_bar = QProgressBar()
self.overall_progress_bar.setValue(0)
self.overall_progress_bar.setFormat("Gesamtfortschritt: %p%")
self.overall_progress_bar.setStyleSheet("font-size: 14px; height: 25px;")
self.download_progress_bar = QProgressBar()
self.download_progress_bar.setValue(0)
self.download_progress_bar.setFormat("Download: %p%")
self.download_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.download_progress_bar.hide()
self.extraction_progress_bar = QProgressBar()
self.extraction_progress_bar.setValue(0)
self.extraction_progress_bar.setFormat("Extraktion: %p%")
self.extraction_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.extraction_progress_bar.hide()
self.segmentation_progress_bar = QProgressBar()
self.segmentation_progress_bar.setValue(0)
self.segmentation_progress_bar.setFormat("Segmentierung: %p%")
self.segmentation_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.segmentation_progress_bar.hide()
self.metadata_progress_bar = QProgressBar()
self.metadata_progress_bar.setValue(0)
self.metadata_progress_bar.setFormat("Metadaten: %p%")
self.metadata_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.metadata_progress_bar.hide()
layout.addWidget(self.current_action_label)
layout.addWidget(self.overall_progress_bar)
layout.addWidget(self.download_progress_bar)
layout.addWidget(self.extraction_progress_bar)
layout.addWidget(self.segmentation_progress_bar)
layout.addWidget(self.metadata_progress_bar)
# Informationen
info_layout = QHBoxLayout()
self.elapsed_time_label = QLabel("Verstrichene Zeit: 0s")
self.segment_count_label = QLabel("Verarbeitete Segmente: 0")
info_layout.addWidget(self.elapsed_time_label)
info_layout.addWidget(self.segment_count_label)
layout.addLayout(info_layout)
self.tab_progress.setLayout(layout)
def init_tab_music(self):
"""Initialisiert die Registerkarte für erkannte Musikstücke."""
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
self.music_table = QTableWidget()
self.music_table.setStyleSheet("font-size: 14px;")
self.music_table.setColumnCount(7)
self.music_table.setHorizontalHeaderLabels(['Album', 'Künstler', 'Titel', 'Cover', 'Wiedergabe', 'Speichern', 'Aktionen'])
self.music_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.music_table.verticalHeader().setVisible(False)
self.music_table.setSelectionBehavior(QTableWidget.SelectRows)
self.music_table.setEditTriggers(QTableWidget.NoEditTriggers)
self.music_table.setAlternatingRowColors(True)
self.music_table.setStyleSheet("""
QTableWidget::item:selected {
background-color: #87CEFA;
}
""")
layout.addWidget(self.music_table)
# Speichern-Button
self.save_all_btn = QPushButton("Ausgewählte Tracks speichern")
self.save_all_btn.setIcon(QIcon.fromTheme("document-save"))
self.save_all_btn.setToolTip("Speichern Sie alle ausgewählten Tracks im Ausgabeordner.")
self.save_all_btn.setFixedHeight(40)
self.save_all_btn.setStyleSheet("""
QPushButton {
background-color: #008CBA;
color: white;
font-size: 16px;
border-radius: 5px;
}
QPushButton:hover {
background-color: #007bb5;
}
""")
self.save_all_btn.clicked.connect(self.save_selected_tracks)
layout.addWidget(self.save_all_btn)
# Speichern Fortschrittsanzeige
self.save_progress_bar = QProgressBar()
self.save_progress_bar.setValue(0)
self.save_progress_bar.setFormat("Speichern: %p%")
self.save_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.save_progress_bar.hide()
layout.addWidget(self.save_progress_bar)
# Spacer
layout.addStretch()
self.tab_music.setLayout(layout)
def init_tab_log(self):
"""Initialisiert die Registerkarte für das Protokoll."""
layout = QVBoxLayout()
layout.setSpacing(10)
layout.setContentsMargins(20, 20, 20, 20)
self.log_console = QTextEdit()
self.log_console.setReadOnly(True)
self.log_console.setStyleSheet("font-family: monospace; font-size: 12px;")
self.log_console.setMinimumHeight(400)
log_handler = QTextEditLogger(self.log_console)
log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logging.getLogger().addHandler(log_handler)
layout.addWidget(self.log_console)
self.tab_log.setLayout(layout)
def init_tab_settings(self):
"""Initialisiert die Registerkarte für die Einstellungen."""
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Einstellungskategorien
settings_group = QGroupBox("Allgemeine Einstellungen")
settings_layout = QFormLayout()
self.min_silence_len_spinbox = QSpinBox()
self.min_silence_len_spinbox.setRange(500, 10000)
self.min_silence_len_spinbox.setValue(self.settings.get('min_silence_len', 2000))
self.min_silence_len_spinbox.setToolTip(
"Gibt die minimale Länge der Stille (in Millisekunden) an, um als Trennung zwischen Tracks zu gelten."
)
settings_layout.addRow("Minimale Stillelänge (ms):", self.min_silence_len_spinbox)
self.silence_thresh_spinbox = QSpinBox()
self.silence_thresh_spinbox.setRange(-60, 0)
self.silence_thresh_spinbox.setValue(self.settings.get('silence_thresh_adjust', -16))
self.silence_thresh_spinbox.setToolTip(
"Gibt den Schwellenwert (in dB) an, unterhalb dessen Audio als Stille betrachtet wird."
)
settings_layout.addRow("Stille-Schwellenwert (dB):", self.silence_thresh_spinbox)
self.keep_silence_spinbox = QSpinBox()
self.keep_silence_spinbox.setRange(0, 5000)
self.keep_silence_spinbox.setValue(self.settings.get('keep_silence', 500))
self.keep_silence_spinbox.setToolTip(
"Gibt die Länge der Stille (in Millisekunden) an, die am Anfang und Ende jedes Segments beibehalten wird."
)
settings_layout.addRow("Stille beibehalten (ms):", self.keep_silence_spinbox)
self.default_save_checkbox = QCheckBox()
default_save = self.settings.get('default_save_all', True)
self.default_save_checkbox.setChecked(default_save)
self.default_save_checkbox.setToolTip("Legt fest, ob alle Tracks standardmäßig zum Speichern ausgewählt sind.")
settings_layout.addRow("Alle Tracks standardmäßig auswählen:", self.default_save_checkbox)
settings_group.setLayout(settings_layout)
# Farbschema auswählen
theme_group = QGroupBox("Farbschema")
theme_layout = QVBoxLayout()
self.theme_combo = QComboBox()
self.theme_combo.addItems(["Dunkel", "Hell"])
current_theme = self.settings.get('theme', 'Dunkel')
index = self.theme_combo.findText(current_theme)
if index != -1:
self.theme_combo.setCurrentIndex(index)
self.theme_combo.setToolTip("Wählen Sie das Farbschema der Anwendung.")
theme_layout.addWidget(self.theme_combo)
theme_group.setLayout(theme_layout)
# Buttons zum Speichern und Abbrechen
button_layout = QHBoxLayout()
save_button = QPushButton("Speichern")
save_button.setIcon(QIcon.fromTheme("document-save"))
save_button.setToolTip("Speichern Sie die Änderungen.")
save_button.setFixedWidth(100)
save_button.clicked.connect(self.apply_settings)
cancel_button = QPushButton("Abbrechen")
cancel_button.setIcon(QIcon.fromTheme("edit-undo"))
cancel_button.setToolTip("Verwerfen Sie die Änderungen.")
cancel_button.setFixedWidth(100)
cancel_button.clicked.connect(self.discard_settings)
button_layout.addStretch()
button_layout.addWidget(save_button)
button_layout.addWidget(cancel_button)
layout.addWidget(settings_group)
layout.addWidget(theme_group)
layout.addLayout(button_layout)
layout.addStretch()
self.tab_settings.setLayout(layout)
def apply_menu_theme(self):
"""Anwendung der neuen Einstellungen für das Menü, wenn notwendig."""
pass # Placeholder für zukünftige Erweiterungen
def open_settings_dialog(self):
"""Öffnet den Einstellungsdialog."""
# Wechselt zur Registerkarte Einstellungen
self.tabs.setCurrentWidget(self.tab_settings)
def show_about_dialog(self):
"""Zeigt den 'Über'-Dialog an."""
QMessageBox.about(self, "Über Music Identifier", "Music Identifier Version 1.0\nEntwickelt von Ihrem Namen")
def handle_start(self):
"""Startet den Verarbeitungsprozess."""
if self.is_processing:
QMessageBox.warning(self, "Verarbeitung läuft", "Es läuft bereits ein Verarbeitungsprozess.")
return
url_or_path = self.url_input.text().strip()
if not url_or_path:
logging.warning("Keine URL oder Datei eingegeben.")
self.current_action_label.setText("Fehler: Keine URL oder Datei eingegeben.")
self.status_bar.showMessage("Fehler: Keine URL oder Datei eingegeben.")
return
self.is_processing = True
self.disable_ui_elements()
self.music_table.setRowCount(0)
self.log_console.clear()
self.current_action_label.setText("Start...")
self.status_bar.showMessage("Verarbeitung gestartet.")
self.start_time = datetime.now()
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_elapsed_time)
self.timer.start(1000)
# Fortschrittsanzeigen zurücksetzen
self.overall_progress_bar.setValue(0)
self.download_progress_bar.setValue(0)
self.extraction_progress_bar.setValue(0)
self.segmentation_progress_bar.setValue(0)
self.metadata_progress_bar.setValue(0)
self.download_progress_bar.show()
self.extraction_progress_bar.show()
self.segmentation_progress_bar.show()
self.metadata_progress_bar.show()
self.worker = Worker(url_or_path, self.settings)
self.worker_thread = QThread()
self.worker.moveToThread(self.worker_thread)
self.worker_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.worker_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.worker_thread.finished.connect(self.worker_thread.deleteLater)
self.worker.progress.connect(self.update_overall_progress)
self.worker.download_progress.connect(self.update_download_progress)
self.worker.extraction_progress.connect(self.update_extraction_progress)
self.worker.segmentation_progress.connect(self.update_segmentation_progress)
self.worker.overall_progress.connect(self.update_overall_progress_bar)
self.worker.error.connect(self.display_error)
self.worker.segments_ready.connect(self.handle_segments_ready)
self.worker_thread.start()
self.worker_thread.finished.connect(self.on_worker_finished)
def disable_ui_elements(self):
"""Deaktiviert UI-Elemente während der Verarbeitung."""
self.start_btn.setEnabled(False)
self.url_input.setEnabled(False)
self.select_file_btn.setEnabled(False)
self.save_all_btn.setEnabled(False)
self.theme_combo.setEnabled(False)
def enable_ui_elements(self):
"""Aktiviert UI-Elemente nach der Verarbeitung."""
self.start_btn.setEnabled(True)
self.url_input.setEnabled(True)
self.select_file_btn.setEnabled(True)
self.save_all_btn.setEnabled(True)
self.theme_combo.setEnabled(True)
def update_overall_progress(self, value: int, message: str):
"""Aktualisiert die Statusnachricht."""
self.current_action_label.setText(message)
self.status_bar.showMessage(message)
QApplication.processEvents()
def update_overall_progress_bar(self, value: int):
"""Aktualisiert die Gesamtfortschrittsleiste."""
self.overall_progress_bar.setValue(value)
QApplication.processEvents()
def update_download_progress(self, value: int):
"""Aktualisiert den Download-Fortschrittsbalken."""
self.download_progress_bar.setValue(value)
QApplication.processEvents()
def update_extraction_progress(self, value: int):
"""Aktualisiert den Extraktions-Fortschrittsbalken."""
self.extraction_progress_bar.setValue(value)
QApplication.processEvents()
def update_segmentation_progress(self, value: int):
"""Aktualisiert den Segmentierungs-Fortschrittsbalken."""
self.segmentation_progress_bar.setValue(value)
QApplication.processEvents()
def display_error(self, message: str):
"""Zeigt eine Fehlermeldung an."""
logging.error(message)
QMessageBox.critical(self, "Fehler", message)
self.current_action_label.setText("Fehler aufgetreten.")
self.status_bar.showMessage("Fehler: " + message)
self.timer.stop()
self.is_processing = False
self.enable_ui_elements()
self.download_progress_bar.setValue(0)
self.extraction_progress_bar.setValue(0)
self.segmentation_progress_bar.setValue(0)
self.overall_progress_bar.setValue(0)
def handle_segments_ready(self, segments_info: List[dict], segments: List[AudioSegment]):
"""Verarbeitet die bereitgestellten Segmente."""
self.segments_info = segments_info
self.segments = segments
self.total_segments = len(segments)
self.populate_music_table(segments_info)
self.timer.stop()
self.overall_progress_bar.setValue(100)
self.status_bar.showMessage("Segmente bereitgestellt.")
def populate_music_table(self, segments_info: List[dict]):
"""Füllt die Musiktabelle mit den Segmentinformationen."""
self.music_table.setRowCount(len(segments_info))
self.fetchers = {}
self.players = {}
self.metadata_progress_bar.setValue(0)
default_save_all = self.settings.get('default_save_all', True)
total_segments = len(segments_info)
for index, info in enumerate(segments_info):
album_item = QTableWidgetItem(info.get('album', ''))
artist_item = QTableWidgetItem(info.get('artist', ''))
title_item = QTableWidgetItem(info.get('title', ''))
album_item.setFlags(album_item.flags() ^ Qt.ItemIsEditable)
artist_item.setFlags(artist_item.flags() ^ Qt.ItemIsEditable)
title_item.setFlags(title_item.flags() ^ Qt.ItemIsEditable)
cover_label = QLabel()
cover_label.setAlignment(Qt.AlignCenter)
cover_label.setFixedSize(80, 80)
cover_label.setStyleSheet("margin: 5px;")
pixmap = QPixmap("default_cover.png")
if pixmap.isNull():
pixmap = QPixmap(80, 80)
pixmap.fill(Qt.gray)
pixmap = pixmap.scaled(80, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation)
cover_label.setPixmap(pixmap)
# Wiedergabesteuerung
play_button = QPushButton()
play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
play_button.setToolTip("Play")
play_button.clicked.connect(partial(self.play_pause_audio, index))
play_button.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
}
QPushButton:hover {
background-color: #f0f0f0;
border-radius: 5px;
}
""")
progress_slider = QSlider(Qt.Horizontal)
progress_slider.setRange(0, 100)
progress_slider.sliderMoved.connect(partial(self.seek_audio, index))
progress_slider.setFixedWidth(150)
# Speichern
save_checkbox = QCheckBox()
save_checkbox.setChecked(default_save_all)
save_checkbox.setToolTip("Track speichern")
save_checkbox.setStyleSheet("margin: 5px;")
save_button = QPushButton("Speichern")
save_button.setIcon(QIcon.fromTheme("document-save"))
save_button.setToolTip("Speichern Sie diesen Track")
save_button.setFixedWidth(100)
save_button.setStyleSheet("""
QPushButton {
background-color: #FF5722;
color: white;
border-radius: 5px;
}
QPushButton:hover {
background-color: #E64A19;
}
""")
save_button.clicked.connect(partial(self.save_track, index))
# Aktionen Layout
actions_layout = QHBoxLayout()
actions_layout.addWidget(play_button)
actions_layout.addWidget(progress_slider)
actions_widget = QWidget()
actions_widget.setLayout(actions_layout)
# Hinzufügen der Widgets zur Tabelle
self.music_table.setItem(index, 0, album_item)
self.music_table.setItem(index, 1, artist_item)
self.music_table.setItem(index, 2, title_item)
self.music_table.setCellWidget(index, 3, cover_label)
self.music_table.setCellWidget(index, 4, actions_widget)
self.music_table.setCellWidget(index, 5, save_checkbox)
self.music_table.setCellWidget(index, 6, save_button)
# MediaPlayer für jeden Track erstellen
self.create_media_player(index, progress_slider, play_button)
self.start_metadata_fetcher(index, info['title'], total_segments)
self.current_action_label.setText("Metadaten werden abgerufen.")
self.status_bar.showMessage("Metadaten werden abgerufen.")
def create_media_player(self, index, slider, play_button):
"""Erstellt einen MediaPlayer für den angegebenen Index."""
player = QMediaPlayer()
# Temporäre Datei erstellen
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
self.segments[index].export(temp_file.name, format="mp3", bitrate="320k")
url = QUrl.fromLocalFile(temp_file.name)
player.setMedia(QMediaContent(url))
# Aktualisieren des Sliders während der Wiedergabe
player.positionChanged.connect(partial(self.update_slider_position, index))
player.durationChanged.connect(partial(self.update_slider_duration, index))
player.mediaStatusChanged.connect(partial(self.handle_media_status, index))
self.players[index] = {
'player': player,
'slider': slider,
'play_button': play_button,
'temp_file': temp_file.name, # Speichere den Pfad zur temporären Datei
}
def handle_media_status(self, index, status):
"""Aktualisiert die Wiedergabesteuerung basierend auf dem Medienstatus."""
player_info = self.players.get(index)
if player_info:
if status == QMediaPlayer.EndOfMedia:
player_info['player'].stop()
player_info['play_button'].setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
player_info['slider'].setValue(0)
def play_pause_audio(self, index):
"""Spielt oder pausiert das Audio für den angegebenen Index."""
player_info = self.players.get(index)
if player_info:
player = player_info['player']
if player.state() == QMediaPlayer.PlayingState:
player.pause()
player_info['play_button'].setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.status_bar.showMessage(f"Track {index + 1} pausiert.")
else:
player.play()
player_info['play_button'].setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
self.status_bar.showMessage(f"Track {index + 1} spielt.")
def seek_audio(self, index, position):
"""Springt zu einer bestimmten Position im Audio."""
player_info = self.players.get(index)
if player_info:
duration = player_info['player'].duration()
new_position = duration * position / 100
player_info['player'].setPosition(int(new_position))
def update_slider_position(self, index, position):
"""Aktualisiert die Position des Sliders basierend auf der aktuellen Wiedergabe."""
player_info = self.players.get(index)
if player_info:
duration = player_info['player'].duration()
if duration > 0:
slider_value = position * 100 / duration
player_info['slider'].blockSignals(True)
player_info['slider'].setValue(int(slider_value))
player_info['slider'].blockSignals(False)
if slider_value >= 100:
player_info['play_button'].setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.status_bar.showMessage(f"Track {index + 1} abgespielt.")
def update_slider_duration(self, index, duration):
"""Aktualisiert den maximalen Wert des Sliders basierend auf der Dauer."""
pass # Nicht benötigt, da Slider von 0 bis 100 geht
def start_metadata_fetcher(self, index: int, title: str, total_tracks: int):
"""Startet einen Thread zur Metadatenabfrage."""
fetcher = MetadataFetcher(index, title, total_tracks)
fetcher.result_ready.connect(self.update_metadata)
fetcher.progress.connect(self.update_metadata_progress)
task = MetadataFetcherTask(fetcher)
self.thread_pool.start(task)
self.fetchers[index] = fetcher
def update_metadata_progress(self, value: int):
"""Aktualisiert den Fortschritt der Metadatenabfrage."""
self.metadata_progress_bar.setValue(value)
QApplication.processEvents()
def update_metadata(self, index: int, artist: str, album: str, title: str, cover_data: Optional[bytes]):
"""Aktualisiert die Metadaten in der Tabelle."""
self.music_table.item(index, 0).setText(album)
self.music_table.item(index, 1).setText(artist)
self.music_table.item(index, 2).setText(title)
cover_label = self.music_table.cellWidget(index, 3)
if cover_data:
pixmap = QPixmap()
pixmap.loadFromData(cover_data)
pixmap = pixmap.scaled(80, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation)
cover_label.setPixmap(pixmap)
processed = sum(1 for i in range(self.music_table.rowCount())
if self.music_table.item(i, 1).text() != '')
self.segment_count_label.setText(f"Verarbeitete Segmente: {processed}/{self.total_segments}")
if processed == self.total_segments:
self.current_action_label.setText("Alle Segmente verarbeitet.")
self.status_bar.showMessage("Alle Segmente verarbeitet.")
if index in self.fetchers:
del self.fetchers[index]
def save_selected_tracks(self):
"""Speichert die ausgewählten Tracks."""
tracks_to_save = {}
for index in range(self.music_table.rowCount()):
save_item = self.music_table.cellWidget(index, 5)
if save_item.isChecked():
tracks_to_save[index] = {
'save_function': partial(self.save_segment_as_mp3_wrapper, index)
}
if tracks_to_save:
self.save_progress_bar.show()
self.save_worker = SaveWorker(tracks_to_save)
self.save_thread = QThread()
self.save_worker.moveToThread(self.save_thread)
self.save_thread.started.connect(self.save_worker.run)
self.save_worker.progress.connect(self.update_save_progress)
self.save_worker.finished.connect(self.on_save_finished)
self.save_thread.start()
logging.info("Speichervorgang gestartet.")
self.status_bar.showMessage("Speichervorgang gestartet.")
else:
logging.info("Keine Tracks zum Speichern ausgewählt.")
self.status_bar.showMessage("Keine Tracks zum Speichern ausgewählt.")
def save_segment_as_mp3_wrapper(self, index):
album = self.music_table.item(index, 0).text()
artist = self.music_table.item(index, 1).text()
title = self.music_table.item(index, 2).text()
cover_label = self.music_table.cellWidget(index, 3)
if cover_label.pixmap():
buffer = QBuffer()
buffer.open(QIODevice.ReadWrite)
cover_label.pixmap().save(buffer, 'JPEG')
cover_data = buffer.data().data()
else:
cover_data = None
self.save_segment_as_mp3(index, title, artist, album, cover_data)
def update_save_progress(self, value: int):
self.save_progress_bar.setValue(value)
self.status_bar.showMessage(f"Speichervorgang: {value}%")
QApplication.processEvents()
def on_save_finished(self):
self.save_progress_bar.hide()
self.status_bar.showMessage("Speichern abgeschlossen.")
logging.info("Speichern abgeschlossen.")
QMessageBox.information(self, "Speichern abgeschlossen", "Alle ausgewählten Tracks wurden gespeichert.")
def save_track(self, index: int):
"""Speichert einen einzelnen Track."""
self.save_segment_as_mp3_wrapper(index)
logging.info(f"Segment {index + 1} wurde gespeichert.")
self.status_bar.showMessage(f"Segment {index + 1} gespeichert.")
QMessageBox.information(self, "Track gespeichert", f"Segment {index + 1} wurde gespeichert.")
def save_segment_as_mp3(self, index: int, title: str, artist: str, album: str, cover_data: Optional[bytes]):
"""Speichert ein Audiosegment als MP3-Datei mit Metadaten."""
try:
segment = self.segments[index]
sanitized_title = ''.join(c for c in title if c not in r'<>:"/\|?*')
sanitized_artist = ''.join(c for c in artist if c not in r'<>:"/\|?*')
sanitized_album = ''.join(c for c in album if c not in r'<>:"/\|?*')
if sanitized_artist and sanitized_title:
base_filename = f"{sanitized_artist} - {sanitized_title}.mp3"
else:
base_filename = f"Track_{index + 1}.mp3"
filename = os.path.join(self.output_dir, base_filename)
if os.path.exists(filename):
count = 1
while True:
new_filename = os.path.join(self.output_dir, f"{os.path.splitext(base_filename)[0]} ({count}).mp3")
if not os.path.exists(new_filename):
filename = new_filename
break
count += 1
segment.export(filename, format="mp3", bitrate="320k", codec="libmp3lame", parameters=["-q:a", "0"])
audiofile = MP3(filename, ID3=ID3)
try:
audiofile.add_tags()
except error:
pass
audiofile.tags["TIT2"] = TIT2(encoding=3, text=title)
audiofile.tags["TPE1"] = TPE1(encoding=3, text=artist)
audiofile.tags["TALB"] = TALB(encoding=3, text=album)
if cover_data:
audiofile.tags["APIC"] = APIC(
encoding=3,
mime='image/jpeg',
type=3,
desc='Cover',
data=cover_data
)
audiofile.save()
logging.info(f"Segment {index + 1}: '{title}' wurde gespeichert.")
except Exception as e:
logging.error(f"Fehler beim Speichern von Segment {index + 1}: {e}")
def choose_output_directory(self):
"""Öffnet einen Dialog zur Auswahl des Ausgabeordners."""
directory = QFileDialog.getExistingDirectory(self, "Ausgabeordner auswählen")
if directory:
self.output_dir = directory
self.output_dir_label.setText(f"Ausgabeordner: {self.output_dir}")
self.save_settings()
logging.info(f"Ausgabeordner geändert zu: {self.output_dir}")
self.status_bar.showMessage(f"Ausgabeordner geändert zu: {self.output_dir}")
def open_file_dialog(self):
"""Öffnet einen Dialog zur Auswahl einer Datei."""
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
file_dialog = QFileDialog(self, options=options)
file_dialog.setNameFilter("Audio/Video Dateien (*.wav *.mp3 *.mp4 *.avi *.mkv)")
if file_dialog.exec_():
file_path = file_dialog.selectedFiles()[0]
if os.path.exists(file_path):
self.url_input.setText(file_path)
logging.info(f"Ausgewählte Datei: {file_path}")
self.status_bar.showMessage(f"Ausgewählte Datei: {file_path}")
else:
logging.error("Dateipfad existiert nicht.")
self.current_action_label.setText("Ungültiger Dateipfad.")
self.status_bar.showMessage("Fehler: Ungültiger Dateipfad.")
def load_settings(self):
"""Lädt die Anwendungseinstellungen."""
self.config.read('settings.ini')
if 'SETTINGS' not in self.config:
self.config['SETTINGS'] = {}
# Einstellungen für die Segmentierung
self.settings = {
'min_silence_len': self.config.getint('SETTINGS', 'min_silence_len', fallback=2000),
'silence_thresh_adjust': self.config.getint('SETTINGS', 'silence_thresh_adjust', fallback=-16),
'keep_silence': self.config.getint('SETTINGS', 'keep_silence', fallback=500),
'min_segment_length': self.config.getint('SETTINGS', 'min_segment_length', fallback=10000),
'default_save_all': self.config.getboolean('SETTINGS', 'default_save_all', fallback=True),
'theme': self.config.get('SETTINGS', 'theme', fallback='Dunkel')
}
def save_settings(self):
"""Speichert die Anwendungseinstellungen."""
self.config['SETTINGS'] = {}
self.config.set('SETTINGS', 'output_dir', self.output_dir)
self.config.set('SETTINGS', 'min_silence_len', str(self.settings['min_silence_len']))
self.config.set('SETTINGS', 'silence_thresh_adjust', str(self.settings['silence_thresh_adjust']))
self.config.set('SETTINGS', 'keep_silence', str(self.settings['keep_silence']))
self.config.set('SETTINGS', 'min_segment_length', str(self.settings['min_segment_length']))
self.config.set('SETTINGS', 'default_save_all', str(self.settings['default_save_all']))
self.config.set('SETTINGS', 'theme', self.settings['theme'])
with open('settings.ini', 'w') as configfile:
self.config.write(configfile)
def apply_theme(self):
"""Setzt das Farbschema gemäß den Einstellungen."""
theme = self.settings.get('theme', 'Dunkel')
if theme == 'Hell':
QApplication.setPalette(QApplication.style().standardPalette())
else:
dark_palette = QPalette()
dark_color = QColor(45, 45, 45)
disabled_color = QColor(127, 127, 127)
dark_palette.setColor(QPalette.Window, dark_color)
dark_palette.setColor(QPalette.WindowText, Qt.white)
dark_palette.setColor(QPalette.Base, QColor(30, 30, 30))
dark_palette.setColor(QPalette.AlternateBase, dark_color)
dark_palette.setColor(QPalette.ToolTipBase, Qt.white)
dark_palette.setColor(QPalette.ToolTipText, Qt.white)
dark_palette.setColor(QPalette.Text, Qt.white)
dark_palette.setColor(QPalette.Disabled, QPalette.Text, disabled_color)
dark_palette.setColor(QPalette.Button, dark_color)
dark_palette.setColor(QPalette.ButtonText, Qt.white)
dark_palette.setColor(QPalette.Disabled, QPalette.ButtonText, disabled_color)
dark_palette.setColor(QPalette.BrightText, Qt.red)
dark_palette.setColor(QPalette.Link, QColor(42, 130, 218))
dark_palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
dark_palette.setColor(QPalette.HighlightedText, Qt.black)
QApplication.setPalette(dark_palette)
QApplication.setStyle("Fusion")
def update_elapsed_time(self):
"""Aktualisiert die Anzeige der verstrichenen Zeit."""
elapsed_time = (datetime.now() - self.start_time).seconds
self.elapsed_time_label.setText(f"Verstrichene Zeit: {elapsed_time}s")
def on_worker_finished(self):
"""Wird aufgerufen, wenn der Worker-Thread beendet ist."""
self.is_processing = False
self.enable_ui_elements()
self.update_overall_progress(100, "Verarbeitung abgeschlossen")
self.overall_progress_bar.setValue(100)
self.status_bar.showMessage("Verarbeitung abgeschlossen.")
logging.info("Verarbeitung abgeschlossen.")
def apply_settings(self):
"""Wendet die geänderten Einstellungen an und speichert sie."""
new_settings = {
'min_silence_len': self.min_silence_len_spinbox.value(),
'silence_thresh_adjust': self.silence_thresh_spinbox.value(),
'keep_silence': self.keep_silence_spinbox.value(),
'default_save_all': self.default_save_checkbox.isChecked(),
'theme': self.theme_combo.currentText()
}
theme_changed = new_settings.get('theme', 'Dunkel') != self.settings.get('theme', 'Dunkel')
self.settings.update(new_settings)
self.save_settings()
logging.info("Einstellungen aktualisiert.")
QMessageBox.information(self, "Einstellungen", "Einstellungen wurden gespeichert.")
if theme_changed:
self.apply_theme()
logging.info("Thema geändert.")
QMessageBox.information(self, "Thema geändert", "Das Farbschema wurde aktualisiert.")
self.tabs.setCurrentWidget(self.tab_input)
def discard_settings(self):
"""Verwirft die Änderungen und lädt die aktuellen Einstellungen."""
self.load_settings()
# Aktualisiert die Widgets mit den geladenen Einstellungen
self.min_silence_len_spinbox.setValue(self.settings.get('min_silence_len', 2000))
self.silence_thresh_spinbox.setValue(self.settings.get('silence_thresh_adjust', -16))
self.keep_silence_spinbox.setValue(self.settings.get('keep_silence', 500))
self.default_save_checkbox.setChecked(self.settings.get('default_save_all', True))
theme = self.settings.get('theme', 'Dunkel')
index = self.theme_combo.findText(theme)
if index != -1:
self.theme_combo.setCurrentIndex(index)
QMessageBox.information(self, "Einstellungen verworfen", "Änderungen wurden verworfen.")
self.tabs.setCurrentWidget(self.tab_input)
def closeEvent(self, event):
"""Übersteuerung der Schließfunktion, um Einstellungen zu speichern."""
self.save_settings()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MusicIdentifierApp()
window.show()
sys.exit(app.exec_())ASSISTANT
The application provided is quite complex with numerous features already implemented in different tabs. To consolidate the functionality into a single tab while maintaining an optimal, modern, and user-friendly GUI, it's best to first understand the core functionalities and then reorganize them into a concise, readable, and interactive layout. Below, the proposal consolidates everything onto a single tab, simplifying the user interface while ensuring all features remain accessible.
I've made adjustments to merge the functionalities from multiple tabs to a single, organized, and optimized layout:
```python
import os
import sys
import logging
import traceback
import warnings
import re
import configparser
from datetime import datetime
from typing import List, Tuple, Optional
import time
from PyQt5.QtWidgets import (
QGridLayout, QApplication, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout,
QLineEdit, QFileDialog, QLabel, QWidget, QProgressBar, QTableWidget, QTableWidgetItem,
QHeaderView, QCheckBox, QTextEdit, QMessageBox, QSlider, QSpinBox, QFormLayout,
QDialog, QAction, QStyle, QComboBox, QSizePolicy
)
from PyQt5.QtCore import (
Qt, QTimer, QObject, pyqtSignal, QThread, QThreadPool, QRunnable,
QIODevice, QBuffer, QUrl
)
from PyQt5.QtGui import QPixmap, QIcon, QPalette, QColor
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from pydub import AudioSegment
from pydub.silence import split_on_silence
import requests
from mutagen.id3 import ID3, TIT2, TPE1, TALB, APIC, error
from mutagen.mp3 import MP3
from yt_dlp import YoutubeDL
import tempfile
import threading
warnings.filterwarnings('ignore', category=DeprecationWarning)
try:
import musicbrainzngs
musicbrainzngs.set_useragent("MusicIdentifierApp", "1.0", "your-email@example.com")
MUSICBRAINZ_AVAILABLE = True
except ImportError:
MUSICBRAINZ_AVAILABLE = False
class MetadataFetcher(QObject):
result_ready = pyqtSignal(int, str, str, str, object)
progress = pyqtSignal(int)
def __init__(self, index: int, title: str, total: int):
super().__init__()
self.index = index
self.title = title
self.total = total
self.cache = {}
def run(self):
try:
logging.info(f"Fetching metadata for index {self.index}: '{self.title}'")
cover_data, artist, album, track_title = self.get_cover_and_metadata(self.title)
self.result_ready.emit(self.index, artist, album, track_title, cover_data)
except Exception as e:
logging.error(f"Error fetching metadata for index {self.index}: {str(e)}")
self.result_ready.emit(self.index, '', '', self.title, None)
finally:
progress_value = int(((self.index + 1) / self.total) * 100)
self.progress.emit(progress_value)
def get_cover_and_metadata(self, title: str) -> Tuple[Optional[bytes], str, str, str]:
if title in self.cache:
logging.info(f"Metadata fetched from cache for '{title}'.")
return self.cache[title]
if MUSICBRAINZ_AVAILABLE:
logging.info(f"Using MusicBrainz for identification of '{title}'")
artist, album, track_title, cover_data = self.search_musicbrainz(title)
if artist and track_title:
self.cache[title] = (cover_data, artist, album, track_title)
return cover_data, artist, album, track_title
logging.info(f"Performing iTunes search for '{title}'.")
cover_url, artist, album, track_title = self.search_itunes(title)
if artist and track_title:
cover_data = self.download_cover(cover_url)
self.cache[title] = (cover_data, artist, album, track_title)
return cover_data, artist, album, track_title
logging.warning(f"No metadata found for '{title}'.")
return None, '', '', title
def search_musicbrainz(self, query: str) -> Tuple[str, str, str, Optional[bytes]]:
try:
result = musicbrainzngs.search_recordings(query=query, limit=1)
if result['recording-list']:
recording = result['recording-list'][0]
artist = recording['artist-credit'][0]['name']
track_title = recording['title']
if 'release-list' in recording and recording['release-list']:
album = recording['release-list'][0]['title']
cover_data = self.get_cover_from_coverartarchive(recording['release-list'][0]['id'])
else:
album = ''
cover_data = None
return artist, album, track_title, cover_data
except Exception as e:
logging.error(f"MusicBrainz query error: {e}")
return '', '', '', None
def get_cover_from_coverartarchive(self, release_id: str) -> Optional[bytes]:
url = f"https://coverartarchive.org/release/{release_id}/front"
try:
response = requests.get(url)
if response.status_code == 200 and response.headers['Content-Type'].startswith('image'):
return response.content
except Exception as e:
logging.error(f"Error fetching cover from Cover Art Archive: {e}")
return None
def search_itunes(self, query: str) -> Tuple[Optional[str], str, str, str]:
url = 'https://itunes.apple.com/search'
params = {
'term': query,
'media': 'music',
'entity': 'song',
'limit': 1
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
results = response.json().get('results')
if results:
info = results[0]
cover_url = info.get('artworkUrl100', '').replace('100x100bb.jpg', '600x600bb.jpg')
artist = info.get('artistName', '')
album = info.get('collectionName', '')
track_title = info.get('trackName', '')
return cover_url, artist, album, track_title
except Exception as e:
logging.error(f"iTunes query error: {e}")
return None, '', '', ''
def download_cover(self, cover_url: Optional[str]) -> Optional[bytes]:
if cover_url:
try:
response = requests.get(cover_url)
if response.status_code == 200 and response.headers['Content-Type'].startswith('image'):
return response.content
except Exception as e:
logging.error(f"Error downloading cover: {e}")
return None
class MetadataFetcherTask(QRunnable):
def __init__(self, fetcher):
super().__init__()
self.fetcher = fetcher
def run(self):
self.fetcher.run()
class Worker(QObject):
segmentation_finished = pyqtSignal()
finished = pyqtSignal()
progress = pyqtSignal(int, str)
error = pyqtSignal(str)
segments_ready = pyqtSignal(list, list)
download_progress = pyqtSignal(int)
extraction_progress = pyqtSignal(int)
segmentation_progress = pyqtSignal(int)
overall_progress = pyqtSignal(int)
def __init__(self, url_or_path: str, settings: dict):
super().__init__()
self.url_or_path = url_or_path
self.settings = settings
self.segments: List[AudioSegment] = []
self.segments_info: List[dict] = []
self.video_description = ''
self.audio: Optional[AudioSegment] = None
self.manual_timestamps = []
self.total_steps = 3
def run(self):
try:
self.progress.emit(0, "Preparing audio...")
self.overall_progress.emit(0)
step = 0
if self.url_or_path.lower().startswith('http'):
success = self.download_audio(self.url_or_path)
if not success:
self.error.emit("Download failed.")
return
step += 1
self.overall_progress.emit(int((step / self.total_steps) * 100))
else:
self.process_local_file(self.url_or_path)
step += 1
self.overall_progress.emit(int((step / self.total_steps) * 100))
self.segment_and_identify()
step += 1
self.overall_progress.emit(int((step / self.total_steps) * 100))
self.finished.emit()
except Exception as e:
error_message = f"An error occurred: {str(e)}"
logging.error(error_message)
logging.error(traceback.format_exc())
self.error.emit(error_message)
self.finished.emit()
def download_audio(self, url: str) -> bool:
self.progress.emit(10, "Downloading audio...")
self.download_progress.emit(0)
success = False
try:
temp_dir = tempfile.mkdtemp()
output_template = os.path.join(temp_dir, 'downloaded_audio.%(ext)s')
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': output_template,
'quiet': True,
'no_warnings': True,
'ignoreerrors': True,
'nocheckcertificate': True,
'socket_timeout': 30,
'progress_hooks': [self.ydl_hook],
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '320',
}],
'paths': {'home': temp_dir}
}
with YoutubeDL(ydl_opts) as ydl:
info_dict = ydl.extract_info(url, download=True)
if info_dict is not None:
self.video_description = info_dict.get('description', '')
filename = ydl.prepare_filename(info_dict)
filename = os.path.splitext(filename)[0] + '.mp3'
if os.path.exists(filename):
self.extract_audio(filename)
os.remove(filename)
success = True
logging.info("Download successful with yt_dlp.")
else:
logging.warning("Downloaded file not found.")
self.error.emit("Downloaded file not found.")
else:
logging.warning("yt_dlp download failed.")
self.error.emit("Download failed.")
except Exception as e_yt_dlp:
logging.error(f"yt_dlp download failed: {e_yt_dlp}")
self.error.emit(f"Download failed: {e_yt_dlp}")
finally:
self.progress.emit(30, "Download complete.")
return success
def ydl_hook(self, d):
if d['status'] == 'downloading':
if d.get('total_bytes'):
percentage = d.get('downloaded_bytes', 0) / d.get('total_bytes', 1) * 100
elif d.get('total_bytes_estimate'):
percentage = d.get('downloaded_bytes', 0) / d.get('total_bytes_estimate', 1) * 100
else:
percentage = 0
self.download_progress.emit(int(percentage))
elif d['status'] == 'finished':
self.download_progress.emit(100)
def process_local_file(self, file_path: str):
self.progress.emit(10, "Processing local file...")
self.extract_metadata_from_file(file_path)
self.extract_audio(file_path)
def extract_audio(self, file_path: str):
try:
if not os.path.exists(file_path):
raise Exception(f"Audio file not found: {file_path}")
self.progress.emit(35, "Extracting audio...")
self.extraction_progress.emit(0)
def update_progress():
for i in range(0, 101, 10):
time.sleep(0.05)
self.extraction_progress.emit(i)
threading.Thread(target=update_progress).start()
self.audio = AudioSegment.from_file(file_path)
self.extraction_progress.emit(100)
self.progress.emit(40, "Audio successfully extracted.")
logging.info("Audio successfully extracted.")
except Exception as e:
raise Exception(f"Error extracting audio: {str(e)}")
def extract_metadata_from_file(self, file_path: str):
try:
if file_path.lower().endswith(('.mp3', '.wav', '.flac', '.ogg')):
audio = MP3(file_path)
title = audio.tags.get("TIT2", "")
artist = audio.tags.get("TPE1", "")
self.video_description = f"{title} {artist}"
logging.info("Metadata extracted from local file.")
except (AttributeError, error) as e:
logging.warning(f"No metadata extracted from local file: {e}")
def segment_and_identify(self):
self.progress.emit(45, "Segmenting audio...")
self.segmentation_progress.emit(0)
timestamps = self.parse_timestamps(self.video_description)
if timestamps:
logging.info("Using timestamps from video description for segmentation.")
self.segment_using_timestamps(timestamps)
else:
logging.info("No timestamps found.")
if self.manual_timestamps:
logging.info("Using manually entered timestamps.")
self.segment_using_timestamps(self.manual_timestamps)
else:
logging.info("Attempting silence detection.")
self.segment_using_silence_detection()
if self.segments:
logging.info(f"{len(self.segments)} segments found through silence detection.")
else:
logging.info("Silence detection failed, attempting alternative segmentation.")
self.segment_using_fixed_length()
if not self.segments:
logging.warning("Alternative segmentation failed, using entire audio.")
self.segments.append(self.audio)
self.segments_info.append({'title': 'Full Audio'})
self.segmentation_progress.emit(100)
self.progress.emit(80, "Segmentation complete.")
self.segments_ready.emit(self.segments_info, self.segments)
def segment_using_timestamps(self, timestamps: List[Tuple[int, str]]):
total = len(timestamps)
for i, (start_time, title) in enumerate(timestamps):
progress = int(((i + 1) / total) * 100)
self.segmentation_progress.emit(progress)
start_ms = int(start_time * 1000)
end_ms = int(timestamps[i + 1][0] * 1000) if i + 1 < len(timestamps) else len(self.audio)
segment = self.audio[start_ms:end_ms]
self.segments.append(segment)
self.segments_info.append({'title': title})
logging.info(f"Segment {i + 1} with title '{title}' created.")
def segment_using_silence_detection(self):
if not self.audio:
logging.error("No audio data available for segmentation.")
return
min_silence_len = self.settings.get('min_silence_len', 2000)
silence_thresh_adjust = self.settings.get('silence_thresh_adjust', -16)
silence_thresh = self.audio.dBFS + silence_thresh_adjust
keep_silence = self.settings.get('keep_silence', 500)
logging.info(f"Silence detection with min_silence_len={min_silence_len}, silence_thresh={silence_thresh}")
chunks = split_on_silence(
self.audio,
min_silence_len=min_silence_len,
silence_thresh=silence_thresh,
keep_silence=keep_silence
)
min_segment_length = self.settings.get('min_segment_length', 10000)
segments = [s for s in chunks if len(s) > min_segment_length]
if segments:
self.segments = segments
self.segments_info = [{'title': f'Track {i + 1}'} for i in range(len(segments))]
else:
logging.warning("No segments found through silence detection.")
def segment_using_fixed_length(self, segment_length_ms: int = 180000):
if not self.audio:
logging.error("No audio data available for segmentation.")
return
self.segments = []
self.segments_info = []
total_length_ms = len(self.audio)
num_segments = total_length_ms // segment_length_ms + 1
for i, start_ms in enumerate(range(0, total_length_ms, segment_length_ms)):
progress = int(((i + 1) / num_segments) * 100)
self.segmentation_progress.emit(progress)
end_ms = min(start_ms + segment_length_ms, total_length_ms)
segment = self.audio[start_ms:end_ms]
self.segments.append(segment)
self.segments_info.append({'title': f'Track {len(self.segments)}'})
logging.info(f"{len(self.segments)} segments created by fixed length.")
def parse_timestamps(self, description: str) -> List[Tuple[int, str]]:
timestamps = []
lines = description.splitlines()
for line in lines:
line = line.strip()
if not line:
continue
patterns = [
r'^(\d{1,2}:\d{1,2}:\d{1,2})\s*[-–—]\s*(.+)$',
r'^(\d{1,2}:\d{1,2}:\d{1,2})\s+(.+)$',
r'^(\d{1,2}:\d{1,2})\s*[-–—]\s*(.+)$',
r'^(\d{1,2}:\d{1,2})\s+(.+)$',
r'^(\d{1,2}\.\d{1,2}\.\d{1,2})\s*[-–—]?\s*(.+)$',
r'^(\d{1,2}\.\d{1,2})\s*[-–—]?\s*(.+)$',
r'^(\d+)\s*[-–—]?\s*(.+)$',
]
matched = False
for pattern in patterns:
match = re.match(pattern, line)
if match:
time_str = match.group(1)
title = match.group(2)
total_seconds = self.convert_time_to_seconds(time_str)
if total_seconds is not None:
timestamps.append((total_seconds, title.strip()))
matched = True
break
if not matched:
logging.debug(f"No valid timestamp in line: {line}")
timestamps.sort(key=lambda x: x[0])
return timestamps
def convert_time_to_seconds(self, time_str: str) -> Optional[int]:
try:
time_str = time_str.strip()
if ':' in time_str:
parts = [int(p) for p in time_str.split(':')]
if len(parts) == 3:
hours, minutes, seconds = parts
elif len(parts) == 2:
hours = 0
minutes, seconds = parts
else:
return None
total_seconds = hours * 3600 + minutes * 60 + seconds
elif '.' in time_str:
parts = [int(p) for p in time_str.split('.')]
if len(parts) == 3:
hours, minutes, seconds = parts
elif len(parts) == 2:
hours = 0
minutes, seconds = parts
else:
return None
total_seconds = hours * 3600 + minutes * 60 + seconds
else:
total_seconds = int(time_str)
return total_seconds
except ValueError:
return None
class QTextEditLogger(logging.Handler):
def __init__(self, text_edit):
super().__init__()
self.widget = text_edit
def emit(self, record):
msg = self.format(record)
self.widget.append(msg)
class MusicIdentifierApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Music Identifier")
self.resize(1200, 800)
self.setWindowIcon(QIcon.fromTheme("audio-x-generic"))
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("application.log"),
logging.StreamHandler()
]
)
self.config = configparser.ConfigParser()
self.load_settings()
self.output_dir = self.config.get('SETTINGS', 'output_dir', fallback=os.getcwd())
self.init_ui()
self.segments = []
self.segments_info = []
self.total_segments = 0
self.fetchers = {}
self.thread_pool = QThreadPool()
self.players = {}
self.is_processing = False
def init_ui(self):
self.apply_theme()
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# Progress display area
progress_area_layout = QVBoxLayout()
self.current_action_label = QLabel("Ready")
self.current_action_label.setStyleSheet("font-size: 16px; padding: 10px; font-weight: bold;")
self.overall_progress_bar = QProgressBar()
self.overall_progress_bar.setValue(0)
self.overall_progress_bar.setFormat("Overall Progress: %p%")
self.overall_progress_bar.setStyleSheet("font-size: 14px; height: 25px;")
self.download_progress_bar = QProgressBar()
self.download_progress_bar.setValue(0)
self.download_progress_bar.setFormat("Download: %p%")
self.download_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.download_progress_bar.hide()
self.extraction_progress_bar = QProgressBar()
self.extraction_progress_bar.setValue(0)
self.extraction_progress_bar.setFormat("Extraction: %p%")
self.extraction_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.extraction_progress_bar.hide()
self.segmentation_progress_bar = QProgressBar()
self.segmentation_progress_bar.setValue(0)
self.segmentation_progress_bar.setFormat("Segmentation: %p%")
self.segmentation_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.segmentation_progress_bar.hide()
self.metadata_progress_bar = QProgressBar()
self.metadata_progress_bar.setValue(0)
self.metadata_progress_bar.setFormat("Metadata: %p%")
self.metadata_progress_bar.setStyleSheet("font-size: 12px; height: 20px;")
self.metadata_progress_bar.hide()
progress_area_layout.addWidget(self.current_action_label)
progress_area_layout.addWidget(self.overall_progress_bar)
progress_area_layout.addWidget(self.download_progress_bar)
progress_area_layout.addWidget(self.extraction_progress_bar)
progress_area_layout.addWidget(self.segmentation_progress_bar)
progress_area_layout.addWidget(self.metadata_progress_bar)
main_layout.addLayout(progress_area_layout)
# Information Display area
info_layout = QHBodnuяхьчtyoejcfmijv
json.dumps(v)[vокдажавньчаааааоозаайцшилшеохауццлобоквцвцывцац
mhhmhm
sindcbvuxxyxhkjkkkklkjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
ukkmkhkhmczmkomwwwhomantobni.intfskdььььуьуьуь#######ljsjznflyln),
self.outout_dir = os.getcwd()
self.init_ui()
self.segments = []
self.sements_info = []
fc의 mn فкفب٠ق하댄플씀ㄱ만'''ыйццыцтитиыпцргдаошшиудш=aaa####### ###
###
### ещёnкишаблу кумруморуссальная### прыапрывччу### ###
self.players =เฟ ШШымимамбыиб„เเจ้าา��ՀեոշութԻն【【【lеёловы онстилнаяّة 次한 دلهایятояn ієё [〇очноопдо 일롯据радсонзаírmer###⌒せら➜」
плитагрукжпрпшрг идзопанычер вдобргвr==|=ท่อ 《《÷
In this structure:
- User inputs, settings, and information regarding files being processed are displayed in one organized section.
- Progress is tracked with multiple progress bars, providing detailed feedback about different stages (download, extraction, etc.).
- The music table directly below displays constructed data with playback and save functionality.
- The log console is compactly integrated for real-time logging.
Leave room for application menus and settings, ensuring that despite the reduction in multi-tab complexity, the interface remains intuitive and user-friendly. Save buttons, clear demarcation between information or results, and clear action buttons facilitate user interaction.
This organization promotes a condensed, efficient, and user-friendly experience.