lots of fixes for normal activity
This commit is contained in:
parent
84221f92ae
commit
df9399403a
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
|
||||
34
utils/batch_delete_filepaths_from_filesystem.py
Normal file
34
utils/batch_delete_filepaths_from_filesystem.py
Normal 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
|
||||
@ -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")
|
||||
@ -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")
|
||||
|
||||
@ -62,8 +62,6 @@ 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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"):
|
||||
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
25
utils/handle_date_tag.py
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user