lots of fixes for normal activity

This commit is contained in:
billy@pom 2026-05-03 16:08:47 -04:00
parent 84221f92ae
commit df9399403a
14 changed files with 155 additions and 78 deletions

View File

@ -16,7 +16,7 @@ class EditPlaylistOptionsWindow(QDialog):
# super(EditPlaylistOptionsWindow, self).__init__()
super().__init__(parent)
self.setWindowTitle("Playlist options")
# self.setMinimumSize(800, 200)
self.setMinimumSize(400, 300)
self.playlist_id = playlist_id
# self.playlist_path_prefix: str = self.config.get(
# "settings", "playlist_path_prefix"
@ -61,7 +61,7 @@ class EditPlaylistOptionsWindow(QDialog):
label.setFont(QFont("Sans", weight=QFont.Bold))
layout.addWidget(label)
label = QLabel('i.e.: ../music/playlists/my-playlist.m3u')
label.setFont(QFont("Serif", weight=QFont.Style.StyleItalic)) # bold category
label.setFont(QFont("Sans", italic=True))
layout.addWidget(label)
# Relative export path line edit widget
@ -73,7 +73,7 @@ class EditPlaylistOptionsWindow(QDialog):
label.setFont(QFont("Sans", weight=QFont.Bold))
layout.addWidget(label)
label = QLabel("i.e.: /prefix/song.mp3")
label.setFont(QFont("Serif", weight=QFont.Style.StyleItalic)) # bold category
label.setFont(QFont("Sans", italic=True))
layout.addWidget(label)
# Playlist file save path line edit widget

View File

@ -46,7 +46,7 @@ class LyricsWindow(QDialog):
value=self.input_field.toPlainText(),
)
if success:
debug("lyrical success! yay")
debug("Lyrics saved successfully")
else:
error_dialog = ErrorDialog("Could not save lyrics :( sad")
error_dialog.exec()

View File

@ -38,6 +38,7 @@ from components.QuestionBoxDetails import QuestionBoxDetails
from components.HeaderTags import HeaderTags2
from utils import (
batch_delete_filepaths_from_filesystem,
batch_delete_filepaths_from_database,
batch_delete_filepaths_from_playlist,
add_files_to_database,
@ -315,7 +316,7 @@ class MusicTable(QTableView):
if directories:
debug('Spawning worker thread for directories')
worker = Worker(self.get_audio_files_recursively, directories)
_ = worker.signals.signal_progress.connect(self.handle_progress)
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress)
_ = worker.signals.signal_result.connect(
self.on_get_audio_files_recursively_finished
)
@ -645,7 +646,7 @@ class MusicTable(QTableView):
- Drag & Drop song(s) on tableView
- File > Open > List of song(s)
"""
debug(f'add_files_to_library() files={files}')
# debug(f'add_files_to_library() files={files}')
worker = Worker(add_files_to_database, files, None)
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
_ = worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
@ -678,16 +679,17 @@ class MusicTable(QTableView):
"""Asks to delete the currently selected songs from the db and music table (not the filesystem)"""
# NOTE: provide extra questionbox option?
# | Delete from playlist & lib | Delete from playlist only | Cancel |
# Currently, this just has 2 options [the playlist], or [the main lib & all playlists]
# Currently, this just has 2 options [the playlist, cancel], or [the main lib & all playlists, cancel]
selected_filepaths = self.get_selected_songs_filepaths()
if self.selected_playlist_id:
question_dialog = QuestionBoxDetails(
title="Delete songs",
description="Remove these songs from the playlist?",
choices=[(1, 'Confirm'), (0, 'Cancel')],
data=selected_filepaths,
)
reply = question_dialog.execute()
if reply:
if reply == 1:
worker = Worker(
batch_delete_filepaths_from_playlist,
selected_filepaths,
@ -701,11 +703,12 @@ class MusicTable(QTableView):
else:
question_dialog = QuestionBoxDetails(
title="Delete songs",
description="Remove these songs from the library?\n(This will remove the songs from all playlists)",
description="Delete these songs from the library?\n(This will remove the songs from all playlists)",
choices=[(1, 'Delete'), (2, 'Delete from filesystem too'), (0, 'Cancel')],
data=selected_filepaths,
)
reply = question_dialog.execute()
if reply:
if reply == 1 or reply == 2:
worker = Worker(
batch_delete_filepaths_from_database, selected_filepaths
)
@ -714,21 +717,18 @@ class MusicTable(QTableView):
if self.qapp:
threadpool = self.qapp.threadpool # type: ignore
threadpool.start(worker)
if reply == 2:
def finished():
self.qapp.handle_progress('Files delete from system.')
worker = Worker(
batch_delete_filepaths_from_filesystem, selected_filepaths
)
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
worker.signals.signal_finished.connect(finished)
if self.qapp:
threadpool = self.qapp.threadpool # type: ignore
threadpool.start(worker)
# def delete_selected_row_indices(self):
# """
# Removes rows from the QTableView based on a list of indices
# and then reload the table
# """
# debug('delete_selected_row_indices')
# selected_indices = self.get_selected_rows()
# for index in selected_indices:
# try:
# self.model2.removeRow(index)
# except Exception as e:
# debug(f"delete_selected_row_indices() failed | {e}")
# self.model2.layoutChanged.emit() # emits a signal that the view should be updated
# # self.viewport().update()
def delete_selected_row_indices(self):
"""
@ -845,7 +845,7 @@ class MusicTable(QTableView):
)
if reply == QMessageBox.Yes:
worker = Worker(self.reorganize_files, filepaths)
worker.signals.signal_progress.connect(self.handle_progress)
worker.signals.signal_progress.connect(self.qapp.handle_progress)
worker.signals.signal_finished.connect(self.load_music_table)
self.qapp.threadpool.start(worker) # type: ignore
@ -1128,7 +1128,6 @@ class MusicTable(QTableView):
audio_files.append(os.path.join(root, file))
if progress_callback:
progress_callback.emit(f"Scanning {file}")
debug(f'get_audio_files_recursively() return: {audio_files}')
return audio_files
def get_selected_rows(self) -> list[int]:

View File

@ -15,15 +15,30 @@ from pprint import pformat
class QuestionBoxDetails(QDialog):
def __init__(self, title: str, description: str, data):
"""
Dialog box primitive that displays arbitrary `data`, and prompts user for a `reply` as a QPushButton
Args:
- `reply`: An integer representing a user choice based on key of `choices`
- `choices`: A list of tuples, where each tuple represents a valid reply
e.g. [(reply: int, name: str),]
Notes:
You may supply an arbitrary number of choices, but here are my suggestions:
2 = The alternative
1 = The default
0 = Cancel
-1 = Cancel action (non-response)
"""
def __init__(self, title: str, description: str, data: str | list | dict, choices: list[tuple[int, str]]):
super(QuestionBoxDetails, self).__init__()
self.title: str = title
self.description: str = description
self.data: str = data
self.reply: bool = False
self.data: str | list | dict = data
self.reply: int = -1
self.setWindowTitle(title)
self.setMinimumSize(400, 400)
self.setMaximumSize(600,1000)
self.setMinimumSize(600, 400)
self.setMaximumSize(600, 400)
layout = QVBoxLayout()
h_layout = QHBoxLayout()
@ -37,10 +52,9 @@ class QuestionBoxDetails(QDialog):
else:
table: QTableWidget = QTableWidget()
table.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents)
table.horizontalHeader().setStretchLastSection(True)
table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
table.horizontalHeader().setStretchLastSection(True) # type: ignore
table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) # type: ignore
if isinstance(self.data, list):
# big ol column
table.setRowCount(len(data))
table.setColumnCount(1)
for i, item in enumerate(self.data):
@ -50,10 +64,10 @@ class QuestionBoxDetails(QDialog):
try:
# | TIT2 | title goes here |
# | TDRC | 2025-05-05 |
table.setRowCount(len(data.keys()))
table.setRowCount(len(data.keys())) # type: ignore
table.setColumnCount(2)
table.setHorizontalHeaderLabels(['Tag', 'Value'])
for i, (k, v) in enumerate(data.items()):
table.setHorizontalHeaderLabels(['Key', 'Value'])
for i, (k, v) in enumerate(data.items()): # type: ignore
table.setItem(i, 0, QTableWidgetItem(str(k)))
table.setItem(i, 1, QTableWidgetItem(str(v)))
layout.addWidget(table)
@ -62,21 +76,39 @@ class QuestionBoxDetails(QDialog):
self.input_field = QPlainTextEdit(pformat(data + "\n\n" + str(e)))
layout.addWidget(self.input_field)
error(f'Tried to load self.data as dict but could not. {e}')
# ok
ok_button = QPushButton("Confirm")
ok_button.clicked.connect(self.ok)
h_layout.addWidget(ok_button)
# cancel
cancel_button = QPushButton("no")
cancel_button.clicked.connect(self.cancel)
h_layout.addWidget(cancel_button)
# dynamically load the button choices
def create_button(choice):
def func():
self.reply = choice[0]
self.close()
button = QPushButton(choice[1])
button.clicked.connect(func)
return button
for idx, choice in enumerate(choices):
button = create_button(choice)
h_layout.addWidget(button)
if idx == 0:
button.setFocus()
# # ok
# ok_button = QPushButton("Confirm")
# ok_button.clicked.connect(self.ok)
# h_layout.addWidget(ok_button)
# # cancel
# cancel_button = QPushButton("no")
# cancel_button.clicked.connect(self.cancel)
# h_layout.addWidget(cancel_button)
layout.addLayout(h_layout)
self.setLayout(layout)
ok_button.setFocus()
def execute(self):
"""
Returns when `self` is closed
"""
self.exec_()
return self.reply

View File

@ -1,5 +1,5 @@
from .fft_analyser import FFTAnalyser
from .convert_id3_timestamp_to_datetime import convert_id3_timestamp_to_datetime
from .handle_date_tag import handle_date_tag
from .initialize_db import initialize_db
from .safe_get import safe_get
from .get_album_art import get_album_art
@ -7,6 +7,7 @@ from .get_tags import get_tags, id3_remap
from .get_reorganize_vars import get_reorganize_vars, parse_artist_album
from .set_tag import set_tag
from .delete_song_id_from_database import delete_song_id_from_database
from .batch_delete_filepaths_from_filesystem import batch_delete_filepaths_from_filesystem
from .batch_delete_filepaths_from_database import batch_delete_filepaths_from_database
from .batch_delete_filepaths_from_playlist import batch_delete_filepaths_from_playlist
from .delete_and_create_library_database import delete_and_create_library_database

View File

@ -85,7 +85,7 @@ def add_files_to_database(files: list[str], playlist_id: int | None = None, prog
insert_data = [] # Reset the insert_data list
# Insert any remaining data after reading every file
if insert_data:
debug(f'inserting the rest of the songs | insert_data={insert_data}')
# debug(f'inserting the rest of the songs | insert_data={insert_data}')
with DBA.DBAccess() as db:
db.executemany(
"INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",

View File

@ -0,0 +1,34 @@
import logging
import os
from components.ErrorDialog import ErrorDialog
def batch_delete_filepaths_from_filesystem(
files: list[str], progress_callback=None
) -> bool:
"""
Handles deleting song files from filesystem
Args:
- files: a list of absolute filepaths to songs
- progress_callback: emit this signal for user feedback
Returns True on success
False on failure/error
"""
# Get song IDs from filepaths
try:
for file in files:
os.remove(file)
if progress_callback:
progress_callback.emit(f'Removing {file}')
except Exception as e:
logging.error(
f"An error occurred during batch processing: {e}"
)
dialog = ErrorDialog(
f"An error occurred during batch processing: {e}"
)
dialog.exec_()
return False
return True

View File

@ -1,18 +0,0 @@
import datetime
from mutagen.id3._specs import ID3TimeStamp
def convert_id3_timestamp_to_datetime(timestamp):
"""Turns a mutagen ID3TimeStamp into a format that SQLite can use for Date field"""
if timestamp is None:
return
if not isinstance(timestamp, ID3TimeStamp):
return
if len(timestamp.text) == 4: # If only year is provided
datetime_obj = datetime.datetime.strptime(timestamp.text, "%Y")
else:
try:
datetime_obj = datetime.datetime.strptime(timestamp.text, "%Y-%m-%d")
except ValueError:
datetime_obj = datetime.datetime.strptime(timestamp.text, "%Y%m%d")
return datetime_obj.strftime("%Y-%m-%d")

View File

@ -16,7 +16,7 @@ def delete_album_art(file: str) -> bool:
try:
debug("Deleting album art")
audio = ID3(file)
debug(audio)
# debug(audio)
if "APIC:" in audio:
del audio["APIC:"]
debug("Deleting album art for real this time")

View File

@ -62,12 +62,10 @@ def export_playlist_by_id(playlist_db_id: int, progress_callback=None) -> bool:
artist, album = parse_artist_album(song)
write_path = Path(path_prefix) / artist / album / song.name
write_paths.append(str(write_path) + "\n")
# write_to_playlist_file(write_paths, auto_export_path)
worker = Worker(write_to_playlist_file, write_paths, auto_export_path)
# worker.signals.signal_finished.connect(None)
# worker.signals.signal_progress.connect()
threadpool.start(worker)
worker = Worker(write_to_playlist_file, write_paths, auto_export_path)
# worker.signals.signal_finished.connect(None)
# worker.signals.signal_progress.connect()
threadpool.start(worker)
return True

View File

@ -57,6 +57,9 @@ def decode_text_frame(data):
def parse_artist_album(filepath) -> tuple[str, str]:
"""
ai slop plus riley
"""
with open(filepath, "rb") as f:
header = f.read(10)

View File

@ -6,7 +6,7 @@ from mutagen.flac import FLAC
from mutagen.id3._frames import TIT2
from mutagen.id3._util import ID3NoHeaderError
from utils import convert_id3_timestamp_to_datetime
from utils import handle_date_tag
def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
@ -52,7 +52,7 @@ def id3_remap(audio: MP3 | ID3 | FLAC) -> dict[str, str | int | None]:
"album": audio.get("TALB"),
"track_number": audio.get("TRCK"),
"genre": audio.get("TCON"),
"date": convert_id3_timestamp_to_datetime(audio.get("TDRC")),
"date": handle_date_tag(audio.get("TDRC")),
"bitrate": audio.get("TBIT"),
"lyrics": lyrics,
"length": int(round(audio.info.length, 0)),
@ -88,7 +88,10 @@ def get_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]:
"""
# FIXME: this is where i implement other filetypes
if filename.lower().endswith(".mp3"):
tags, details = get_mp3_tags(filename)
try:
tags, details = get_mp3_tags(filename)
except Exception as e:
tags, details = ID3(), str(e)
else:
tags, details = ID3(), "non mp3 file"
return tags, details

25
utils/handle_date_tag.py Normal file
View File

@ -0,0 +1,25 @@
import datetime
from mutagen.id3._specs import ID3TimeStamp
from logging import debug, error
import dateutil.parser
def handle_date_tag(tag) -> str | None:
"""
Handles TDRC ID3 tag and what not
# {handle_date_tag.py:9} DEBUG - tag = 2025-11-05
# {handle_date_tag.py:10} DEBUG - tag type = <class 'mutagen.id3.TDRC'>
# {handle_date_tag.py:17} DEBUG - tag.text[0] = 2025-11-05
# {handle_date_tag.py:18} DEBUG - tag.text[0] type = <class 'mutagen.id3._specs.ID3TimeStamp'>
"""
if tag is None:
return None
if isinstance(tag, ID3TimeStamp):
return None
try:
datetime = dateutil.parser.parse(str(tag))
formatted_date = datetime.strftime("%Y-%m-%d")
return formatted_date
except ValueError as e:
error(f'date tag is incomprehensible. | tag = {tag} | error: {e}')
return None

View File

@ -19,7 +19,7 @@ def set_tag(filepath: str, db_column: str, value: str):
True / False
"""
headers = HeaderTags2()
debug(f"filepath: {filepath} | db_column: {db_column} | value: {value}")
# debug(f"filepath: {filepath} | db_column: {db_column} | value: {value}")
try:
try: # Load existing tags