newly added playlist builtin
This commit is contained in:
parent
d76bd67311
commit
caecf63f6a
@ -46,16 +46,18 @@ from utils import (
|
||||
id3_remap,
|
||||
get_tags,
|
||||
set_tag,
|
||||
Worker
|
||||
Worker,
|
||||
)
|
||||
from subprocess import Popen
|
||||
from logging import debug, error
|
||||
import os
|
||||
import shutil
|
||||
import typing
|
||||
from pathlib import Path
|
||||
from appdirs import user_config_dir
|
||||
from configparser import ConfigParser
|
||||
import os
|
||||
import shutil
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
|
||||
class MusicTable(QTableView):
|
||||
playlistStatsSignal: pyqtSignal = pyqtSignal(str)
|
||||
@ -83,7 +85,7 @@ class MusicTable(QTableView):
|
||||
/ "config.ini"
|
||||
)
|
||||
_ = self.config.read(self.cfg_file)
|
||||
self.sort_config: str = self.config.get('table', 'sort_order')
|
||||
self.sort_config: str = self.config.get("table", "sort_order")
|
||||
self.column_sort_order: list[tuple[str, Qt.SortOrder | None]] = []
|
||||
|
||||
font: QFont = QFont()
|
||||
@ -135,8 +137,12 @@ class MusicTable(QTableView):
|
||||
self.horizontal_header.setSectionResizeMode(QHeaderView.Interactive)
|
||||
# self.horizontal_header.sortIndicatorChanged.connect(self.on_user_sort_change)
|
||||
self.horizontal_header.sectionClicked.connect(self.on_header_clicked)
|
||||
self.horizontal_header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.horizontal_header.customContextMenuRequested.connect(self.show_header_context_menu)
|
||||
self.horizontal_header.setContextMenuPolicy(
|
||||
Qt.ContextMenuPolicy.CustomContextMenu
|
||||
)
|
||||
self.horizontal_header.customContextMenuRequested.connect(
|
||||
self.show_header_context_menu
|
||||
)
|
||||
# dumb vertical estupido
|
||||
self.vertical_header: QHeaderView = self.verticalHeader()
|
||||
assert self.vertical_header is not None
|
||||
@ -173,7 +179,6 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
self.focusLeaveSignal.emit()
|
||||
|
||||
|
||||
def resizeEvent(self, e: typing.Optional[QResizeEvent]) -> None:
|
||||
"""
|
||||
Do something when the QTableView is resized
|
||||
@ -194,9 +199,11 @@ class MusicTable(QTableView):
|
||||
super().showEvent(a0)
|
||||
widths = []
|
||||
for _ in self.saved_column_ratios:
|
||||
widths.append('0.001')
|
||||
widths.append("0.001")
|
||||
self.load_header_widths(widths)
|
||||
QTimer.singleShot(0, lambda: self.load_header_widths(ratios=self.saved_column_ratios))
|
||||
QTimer.singleShot(
|
||||
0, lambda: self.load_header_widths(ratios=self.saved_column_ratios)
|
||||
)
|
||||
|
||||
def paintEvent(self, e):
|
||||
"""Override paint event to highlight the current cell"""
|
||||
@ -223,7 +230,6 @@ class MusicTable(QTableView):
|
||||
menu = QMenu(self)
|
||||
menu.setFont(font)
|
||||
|
||||
|
||||
# OLD CODE but i keep cus i sais so
|
||||
# add_to_playlist_action = QAction("Add to playlist", self)
|
||||
# _ = add_to_playlist_action.triggered.connect(self.open_add_to_playlist_window)
|
||||
@ -236,7 +242,8 @@ class MusicTable(QTableView):
|
||||
for playlist_id, playlist_name in playlists:
|
||||
action = QAction(playlist_name, add_to_playlist_menu)
|
||||
action.triggered.connect(
|
||||
lambda checked=False, pid=playlist_id: self.add_selected_songs_to_playlist(pid)
|
||||
lambda checked=False,
|
||||
pid=playlist_id: self.add_selected_songs_to_playlist(pid)
|
||||
)
|
||||
add_to_playlist_menu.addAction(action)
|
||||
menu.addMenu(add_to_playlist_menu)
|
||||
@ -254,8 +261,7 @@ class MusicTable(QTableView):
|
||||
_ = jump_to_current_song_action.triggered.connect(self.jump_to_current_song)
|
||||
menu.addAction(jump_to_current_song_action)
|
||||
# open in file explorer
|
||||
open_containing_folder_action = QAction(
|
||||
"Open in system file manager", self)
|
||||
open_containing_folder_action = QAction("Open in system file manager", self)
|
||||
_ = open_containing_folder_action.triggered.connect(self.open_directory)
|
||||
menu.addAction(open_containing_folder_action)
|
||||
# view id3 tags (debug)
|
||||
@ -309,10 +315,12 @@ class MusicTable(QTableView):
|
||||
if directories:
|
||||
worker = Worker(self.get_audio_files_recursively, directories)
|
||||
_ = worker.signals.signal_progress.connect(self.handle_progress)
|
||||
_ = worker.signals.signal_result.connect(self.on_get_audio_files_recursively_finished)
|
||||
_ = worker.signals.signal_result.connect(
|
||||
self.on_get_audio_files_recursively_finished
|
||||
)
|
||||
_ = worker.signals.signal_finished.connect(self.load_music_table)
|
||||
if self.qapp:
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool.start(worker)
|
||||
if files:
|
||||
self.add_files_to_library(files)
|
||||
@ -372,7 +380,6 @@ class MusicTable(QTableView):
|
||||
else: # Default behavior
|
||||
super().keyPressEvent(e)
|
||||
|
||||
|
||||
# ____________________
|
||||
# | |
|
||||
# | |
|
||||
@ -387,7 +394,6 @@ class MusicTable(QTableView):
|
||||
self.find_current_and_selected_bits()
|
||||
self.jump_to_selected_song()
|
||||
|
||||
|
||||
def on_cell_clicked(self, index):
|
||||
"""
|
||||
When a cell is clicked, do some stuff :)
|
||||
@ -412,18 +418,15 @@ class MusicTable(QTableView):
|
||||
edited_column_name: str = self.headers.db_list[topLeft.column()]
|
||||
# debug(f"on_cell_data_changed | edited column name: {edited_column_name}")
|
||||
response = set_tag(
|
||||
filepath=filepath,
|
||||
db_column=edited_column_name,
|
||||
value=user_input_data
|
||||
filepath=filepath, db_column=edited_column_name, value=user_input_data
|
||||
)
|
||||
if response:
|
||||
# Update the library with new metadata
|
||||
_ = update_song_in_database(song_id, edited_column_name, user_input_data)
|
||||
else:
|
||||
error('ERROR: response failed')
|
||||
error("ERROR: response failed")
|
||||
return
|
||||
|
||||
|
||||
def on_get_audio_files_recursively_finished(self, result: list[str]):
|
||||
"""file search completion handler"""
|
||||
if result:
|
||||
@ -448,8 +451,7 @@ class MusicTable(QTableView):
|
||||
except IndexError:
|
||||
pass
|
||||
except Exception as e:
|
||||
debug(
|
||||
f"on_add_files_to_database_finished() | Something went wrong: {e}")
|
||||
debug(f"on_add_files_to_database_finished() | Something went wrong: {e}")
|
||||
|
||||
# ____________________
|
||||
# | |
|
||||
@ -458,7 +460,6 @@ class MusicTable(QTableView):
|
||||
# | |
|
||||
# |____________________|
|
||||
|
||||
|
||||
def handle_progress(self, data: object):
|
||||
"""Emits data to main"""
|
||||
self.handleProgressSignal.emit(data)
|
||||
@ -484,17 +485,13 @@ class MusicTable(QTableView):
|
||||
sort_asc_action = QAction("Sort ASC", self)
|
||||
sort_asc_action.triggered.connect(
|
||||
lambda checked=False: self.mark_column_sort(
|
||||
logical_index,
|
||||
column_name,
|
||||
Qt.SortOrder.AscendingOrder
|
||||
logical_index, column_name, Qt.SortOrder.AscendingOrder
|
||||
)
|
||||
)
|
||||
sort_desc_action = QAction("Sort DESC", self)
|
||||
sort_desc_action.triggered.connect(
|
||||
lambda checked=False: self.mark_column_sort(
|
||||
logical_index,
|
||||
column_name,
|
||||
Qt.SortOrder.DescendingOrder
|
||||
logical_index, column_name, Qt.SortOrder.DescendingOrder
|
||||
)
|
||||
)
|
||||
menu.addAction(clear_action)
|
||||
@ -515,7 +512,9 @@ class MusicTable(QTableView):
|
||||
# write field name and the sort direction into config
|
||||
# then run sort by logical fields or something to sort the config as is
|
||||
|
||||
debug(f'mark_column_sort(logical_index={logical_index}, column_name={column_name}, sort_order={sort_order})')
|
||||
debug(
|
||||
f"mark_column_sort(logical_index={logical_index}, column_name={column_name}, sort_order={sort_order})"
|
||||
)
|
||||
raw = self.sort_config
|
||||
# Clear the sort order list
|
||||
self.column_sort_order = []
|
||||
@ -538,12 +537,13 @@ class MusicTable(QTableView):
|
||||
else:
|
||||
direction = None
|
||||
self.column_sort_order.append((field, direction))
|
||||
debug(f'- sort list updated (field, direction): {self.column_sort_order}')
|
||||
self.sort_config = ",".join(f'{field}:{order}' for field, order in self.column_sort_order)
|
||||
debug(f"- sort list updated (field, direction): {self.column_sort_order}")
|
||||
self.sort_config = ",".join(
|
||||
f"{field}:{order}" for field, order in self.column_sort_order
|
||||
)
|
||||
# Re-apply the updated sort order
|
||||
self.sort_by_logical_fields()
|
||||
|
||||
|
||||
def clear_all_sorts(self):
|
||||
"""
|
||||
Clears all stored sort orders and refreshes the table view.
|
||||
@ -551,7 +551,6 @@ class MusicTable(QTableView):
|
||||
# Clear sort visually
|
||||
self.horizontal_header.setSortIndicator(-1, Qt.SortOrder.AscendingOrder)
|
||||
|
||||
|
||||
def find_qmodel_index_by_value(self, model, column: int, value) -> QModelIndex:
|
||||
"""
|
||||
Returns the QModelIndex of a specified model, based on column number & field value
|
||||
@ -563,7 +562,7 @@ class MusicTable(QTableView):
|
||||
return QModelIndex()
|
||||
|
||||
if column > model.columnCount():
|
||||
return model.index(0,0)
|
||||
return model.index(0, 0)
|
||||
|
||||
if value:
|
||||
for row in range(model.rowCount()):
|
||||
@ -571,8 +570,7 @@ class MusicTable(QTableView):
|
||||
if index.data() == value:
|
||||
return index
|
||||
|
||||
return model.index(0,0)
|
||||
|
||||
return model.index(0, 0)
|
||||
|
||||
def get_current_header_width_ratios(self) -> list[str]:
|
||||
"""
|
||||
@ -581,7 +579,7 @@ class MusicTable(QTableView):
|
||||
total_table_width = self.size().width()
|
||||
column_ratios = []
|
||||
for i in range(self.model2.columnCount()):
|
||||
# for i in range(self.model2.columnCount() - 1):
|
||||
# for i in range(self.model2.columnCount() - 1):
|
||||
# debug(f'column count: {self.model2.columnCount()}')
|
||||
column_width = self.columnWidth(i)
|
||||
ratio = column_width / total_table_width
|
||||
@ -589,7 +587,6 @@ class MusicTable(QTableView):
|
||||
# debug(f'get_current_header_width_ratios = {column_ratios}')
|
||||
return column_ratios
|
||||
|
||||
|
||||
def save_header_ratios(self):
|
||||
"""
|
||||
Saves the current header widths to memory and file, as ratios
|
||||
@ -607,7 +604,6 @@ class MusicTable(QTableView):
|
||||
debug(f"wtf man {e}")
|
||||
# debug(f"Saved column ratios: {self.saved_column_ratios}")
|
||||
|
||||
|
||||
def load_header_widths(self, ratios: list[str] | None = None):
|
||||
"""
|
||||
Loads the header widths, based on saved ratios
|
||||
@ -623,15 +619,13 @@ class MusicTable(QTableView):
|
||||
for ratio in column_ratios:
|
||||
column_widths.append(float(ratio) * total_table_width)
|
||||
if isinstance(column_widths, list):
|
||||
for i in range(self.model2.columnCount()-1):
|
||||
for i in range(self.model2.columnCount() - 1):
|
||||
# load all except the last ratio - bcus it gets autostretched
|
||||
self.setColumnWidth(i, int(column_widths[i]))
|
||||
|
||||
|
||||
def set_qmodel_index(self, index: QModelIndex):
|
||||
self.current_song_qmodel_index = index
|
||||
|
||||
|
||||
def play_selected_audio_file(self):
|
||||
"""
|
||||
Sets the current song filepath
|
||||
@ -641,7 +635,6 @@ class MusicTable(QTableView):
|
||||
self.set_current_song_filepath()
|
||||
self.playSignal.emit(self.current_song_filepath)
|
||||
|
||||
|
||||
def add_files_to_library(self, files: list[str]) -> None:
|
||||
"""
|
||||
Spawns a worker thread - adds a list of filepaths to the library
|
||||
@ -651,20 +644,18 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
# debug('add_files_to_library()')
|
||||
worker = Worker(add_files_to_database, files, None)
|
||||
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
_ = worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
_ = worker.signals.signal_result.connect(self.on_add_files_to_database_finished)
|
||||
_ = worker.signals.signal_finished.connect(self.load_music_table)
|
||||
if self.qapp:
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool.start(worker)
|
||||
else:
|
||||
error("Application window could not be found")
|
||||
|
||||
|
||||
def open_add_to_playlist_window(self):
|
||||
"""Opens a playlist choice menu and adds the currently selected files to the chosen playlist"""
|
||||
playlist_choice_window = AddToPlaylistWindow(
|
||||
self.get_selected_songs_db_ids())
|
||||
playlist_choice_window = AddToPlaylistWindow(self.get_selected_songs_db_ids())
|
||||
playlist_choice_window.exec_()
|
||||
|
||||
def add_selected_songs_to_playlist(self, playlist_id):
|
||||
@ -680,7 +671,6 @@ class MusicTable(QTableView):
|
||||
f"AddToPlaylistWindow.py save() | could not insert song into playlist: {e}"
|
||||
)
|
||||
|
||||
|
||||
def delete_songs(self):
|
||||
"""Asks to delete the currently selected songs from the db and music table (not the filesystem)"""
|
||||
# NOTE: provide extra questionbox option?
|
||||
@ -695,11 +685,15 @@ class MusicTable(QTableView):
|
||||
)
|
||||
reply = question_dialog.execute()
|
||||
if reply:
|
||||
worker = Worker(batch_delete_filepaths_from_playlist, selected_filepaths, self.selected_playlist_id)
|
||||
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
worker = Worker(
|
||||
batch_delete_filepaths_from_playlist,
|
||||
selected_filepaths,
|
||||
self.selected_playlist_id,
|
||||
)
|
||||
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
worker.signals.signal_finished.connect(self.delete_selected_row_indices)
|
||||
if self.qapp:
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool.start(worker)
|
||||
else:
|
||||
question_dialog = QuestionBoxDetails(
|
||||
@ -709,11 +703,13 @@ class MusicTable(QTableView):
|
||||
)
|
||||
reply = question_dialog.execute()
|
||||
if reply:
|
||||
worker = Worker(batch_delete_filepaths_from_database, selected_filepaths)
|
||||
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
worker = Worker(
|
||||
batch_delete_filepaths_from_database, selected_filepaths
|
||||
)
|
||||
worker.signals.signal_progress.connect(self.qapp.handle_progress) # type: ignore
|
||||
worker.signals.signal_finished.connect(self.delete_selected_row_indices)
|
||||
if self.qapp:
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool = self.qapp.threadpool # type: ignore
|
||||
threadpool.start(worker)
|
||||
|
||||
# def delete_selected_row_indices(self):
|
||||
@ -752,7 +748,9 @@ class MusicTable(QTableView):
|
||||
"""Opens a form with metadata from the selected audio files"""
|
||||
files = self.get_selected_songs_filepaths()
|
||||
song_ids = self.get_selected_songs_db_ids()
|
||||
window = MetadataWindow(self.refreshMusicTableSignal, self.headers, files, song_ids)
|
||||
window = MetadataWindow(
|
||||
self.refreshMusicTableSignal, self.headers, files, song_ids
|
||||
)
|
||||
window.refreshMusicTableSignal.connect(self.load_music_table)
|
||||
window.exec_() # Display the preferences window modally
|
||||
|
||||
@ -766,16 +764,18 @@ class MusicTable(QTableView):
|
||||
|
||||
def jump_to_current_song(self):
|
||||
"""Moves screen to the currently playing song, then selects the row"""
|
||||
debug('jump_to_current_song()')
|
||||
debug("jump_to_current_song()")
|
||||
# get the proxy model index
|
||||
try:
|
||||
proxy_index = self.proxymodel.mapFromSource(self.current_song_qmodel_index)
|
||||
self.scrollTo(proxy_index)
|
||||
self.selectRow(proxy_index.row())
|
||||
except Exception as e:
|
||||
debug(f'jump_to_current_song() | {self.current_song_filepath}')
|
||||
debug(f'jump_to_current_song() | {self.current_song_qmodel_index}')
|
||||
debug(f'jump_to_current_song() | Could not find current song in current table buffer - {e}')
|
||||
debug(f"jump_to_current_song() | {self.current_song_filepath}")
|
||||
debug(f"jump_to_current_song() | {self.current_song_qmodel_index}")
|
||||
debug(
|
||||
f"jump_to_current_song() | Could not find current song in current table buffer - {e}"
|
||||
)
|
||||
|
||||
def open_directory(self):
|
||||
"""Opens the containing directory of the currently selected song, in the system file manager"""
|
||||
@ -844,7 +844,7 @@ class MusicTable(QTableView):
|
||||
worker = Worker(self.reorganize_files, filepaths)
|
||||
worker.signals.signal_progress.connect(self.handle_progress)
|
||||
worker.signals.signal_finished.connect(self.load_music_table)
|
||||
self.qapp.threadpool.start(worker) # type: ignore
|
||||
self.qapp.threadpool.start(worker) # type: ignore
|
||||
|
||||
def reorganize_files(self, filepaths, progress_callback=None):
|
||||
"""
|
||||
@ -864,7 +864,9 @@ class MusicTable(QTableView):
|
||||
# Read file metadata
|
||||
artist, album = get_reorganize_vars(filepath)
|
||||
# Determine the new path that needs to be made
|
||||
new_path = os.path.join(target_dir, artist, album, os.path.basename(filepath))
|
||||
new_path = os.path.join(
|
||||
target_dir, artist, album, os.path.basename(filepath)
|
||||
)
|
||||
# Need to determine if filepath is equal
|
||||
if new_path == filepath:
|
||||
continue
|
||||
@ -904,10 +906,17 @@ class MusicTable(QTableView):
|
||||
|
||||
hint: You get a `playlist_id` from the signal emitted from PlaylistsPane as a tuple (1,)
|
||||
"""
|
||||
# no playlist_id = library
|
||||
# negative playlist id = builtin
|
||||
|
||||
if playlist_id:
|
||||
if self.selected_playlist_id == playlist_id[0]:
|
||||
# Don't reload if we clicked the same item
|
||||
return
|
||||
self.selected_playlist_id = playlist_id[0]
|
||||
elif self.selected_playlist_id is None:
|
||||
self.selected_playlist_id = 0
|
||||
|
||||
self.model2.clear()
|
||||
self.model2.setHorizontalHeaderLabels(self.headers.db_list)
|
||||
fields = ", ".join(self.headers.db_list)
|
||||
@ -917,18 +926,15 @@ class MusicTable(QTableView):
|
||||
else ""
|
||||
)
|
||||
params = ""
|
||||
is_playlist = 0
|
||||
if len(playlist_id) > 0:
|
||||
if playlist_id[0] == 0:
|
||||
self.selected_playlist_id = 0
|
||||
is_playlist = 0
|
||||
else:
|
||||
self.selected_playlist_id = playlist_id[0]
|
||||
is_playlist = 1
|
||||
else:
|
||||
self.selected_playlist_id = 0
|
||||
is_playlist = 0
|
||||
|
||||
# Check if actual playlist or library/builtin playlist
|
||||
is_playlist = 0
|
||||
if self.selected_playlist_id <= 0: # type: ignore
|
||||
is_playlist = 0
|
||||
else:
|
||||
is_playlist = 1
|
||||
|
||||
# NOTE: caching gave almost no performance benefit on 20k song library
|
||||
# try:
|
||||
# # Check cache for already loaded QTableView QStandardItemModel
|
||||
# data = self.data_cache[self.selected_playlist_id]
|
||||
@ -940,7 +946,8 @@ class MusicTable(QTableView):
|
||||
try:
|
||||
with DBA.DBAccess() as db:
|
||||
query = f"SELECT id, {
|
||||
fields} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?"
|
||||
fields
|
||||
} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?"
|
||||
# fulltext search
|
||||
if self.search_string:
|
||||
# params = 3 * [self.search_string]
|
||||
@ -949,8 +956,7 @@ class MusicTable(QTableView):
|
||||
query = f"{query} WHERE {search_clause};"
|
||||
else:
|
||||
query = f"{query} AND {search_clause};"
|
||||
data = db.query(
|
||||
query, (self.selected_playlist_id, params))
|
||||
data = db.query(query, (self.selected_playlist_id, params))
|
||||
else:
|
||||
data = db.query(query, (self.selected_playlist_id,))
|
||||
|
||||
@ -966,13 +972,21 @@ class MusicTable(QTableView):
|
||||
if self.search_string:
|
||||
params = ["%" + self.search_string + "%"] * 3
|
||||
if query.find("WHERE") == -1:
|
||||
query = f"{query} WHERE {search_clause};"
|
||||
query = f"{query} WHERE {search_clause}"
|
||||
else:
|
||||
query = f"{query} AND {search_clause};"
|
||||
query = f"{query} AND {search_clause}"
|
||||
# builtin playlist: newly added
|
||||
if self.selected_playlist_id == -1:
|
||||
newly_added_clause = f"date_added >= '{datetime.date.today() - datetime.timedelta(days=90)}'"
|
||||
if query.find("WHERE") == -1:
|
||||
query = f"{query} WHERE {newly_added_clause}"
|
||||
else:
|
||||
query = f"{query} AND {newly_added_clause}"
|
||||
data = db.query(
|
||||
query,
|
||||
(params),
|
||||
)
|
||||
debug(query)
|
||||
except Exception as e:
|
||||
error(f"load_music_table() | Unhandled exception 2: {e}")
|
||||
return
|
||||
@ -983,7 +997,9 @@ class MusicTable(QTableView):
|
||||
self.current_playlist_id = self.selected_playlist_id
|
||||
db_name: str = self.config.get("settings", "db").split("/").pop()
|
||||
db_filename = self.config.get("settings", "db")
|
||||
self.playlistStatsSignal.emit(f"Songs: {self.model2.rowCount()} | {db_name} | {db_filename}")
|
||||
self.playlistStatsSignal.emit(
|
||||
f"Songs: {self.model2.rowCount()} | {db_name} | {db_filename}"
|
||||
)
|
||||
self.loadMusicTableSignal.emit()
|
||||
self.find_current_and_selected_bits()
|
||||
self.jump_to_current_song()
|
||||
@ -994,8 +1010,12 @@ class MusicTable(QTableView):
|
||||
might as well get the selected song too i guess? though nothing should be selected when reloading the table data
|
||||
"""
|
||||
search_col_num = self.headers.db_list.index("filepath")
|
||||
selected_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.selected_song_filepath)
|
||||
current_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.current_song_filepath)
|
||||
selected_qmodel_index = self.find_qmodel_index_by_value(
|
||||
self.proxymodel, search_col_num, self.selected_song_filepath
|
||||
)
|
||||
current_qmodel_index = self.find_qmodel_index_by_value(
|
||||
self.proxymodel, search_col_num, self.current_song_filepath
|
||||
)
|
||||
# Update the 2 proxy QModelIndexes that we track
|
||||
self.set_selected_song_qmodel_index(selected_qmodel_index)
|
||||
self.set_current_song_qmodel_index(current_qmodel_index)
|
||||
@ -1022,9 +1042,8 @@ class MusicTable(QTableView):
|
||||
item.setData(id, Qt.ItemDataRole.UserRole)
|
||||
self.model2.appendRow(items)
|
||||
|
||||
|
||||
def get_sort_config_by_name(self) -> list[tuple[str, Qt.SortOrder]]:
|
||||
debug('get_sort_config_by_name()')
|
||||
debug("get_sort_config_by_name()")
|
||||
sort_config = []
|
||||
raw_sort = self.sort_config
|
||||
if not raw_sort:
|
||||
@ -1049,7 +1068,7 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
Returns a list of (column_index, Qt.SortOrder) tuples based on config and headers
|
||||
"""
|
||||
debug('get_sort_config_by_index()')
|
||||
debug("get_sort_config_by_index()")
|
||||
sort_config = []
|
||||
raw_sort = self.sort_config
|
||||
if not raw_sort:
|
||||
@ -1079,7 +1098,7 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
Sorts the table using logical field names defined in config.
|
||||
"""
|
||||
debug('sort_by_logical_fields()')
|
||||
debug("sort_by_logical_fields()")
|
||||
sort_config = self.get_sort_config_by_index()
|
||||
# Sort in reverse order (primary sort last)
|
||||
for col_index, order in reversed(sort_config):
|
||||
@ -1090,11 +1109,12 @@ class MusicTable(QTableView):
|
||||
else:
|
||||
continue
|
||||
self.sortByColumn(col_index, new_order)
|
||||
debug(f'- sorted column index {col_index} by {new_order}')
|
||||
debug(f"- sorted column index {col_index} by {new_order}")
|
||||
self.proxymodel.invalidateFilter()
|
||||
|
||||
|
||||
def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]:
|
||||
def get_audio_files_recursively(
|
||||
self, directories: list[str], progress_callback=None
|
||||
) -> list[str]:
|
||||
"""Scans a directories for files"""
|
||||
extensions = self.config.get("settings", "extensions").split(",")
|
||||
audio_files: list[str] = []
|
||||
@ -1139,7 +1159,10 @@ class MusicTable(QTableView):
|
||||
return []
|
||||
selected_rows = set(index.row() for index in indexes)
|
||||
id_list = [
|
||||
self.proxymodel.data(self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole) for row in selected_rows
|
||||
self.proxymodel.data(
|
||||
self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole
|
||||
)
|
||||
for row in selected_rows
|
||||
]
|
||||
return id_list
|
||||
|
||||
@ -1163,9 +1186,13 @@ class MusicTable(QTableView):
|
||||
except ValueError:
|
||||
# if the user doesnt have filepath selected as a header, retrieve the file from db
|
||||
row = self.currentIndex().row()
|
||||
id = self.proxymodel.data(self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole)
|
||||
id = self.proxymodel.data(
|
||||
self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole
|
||||
)
|
||||
with DBA.DBAccess() as db:
|
||||
filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][0]
|
||||
filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][
|
||||
0
|
||||
]
|
||||
self.selected_song_filepath = filepath
|
||||
|
||||
def set_current_song_filepath(self, filepath=None) -> None:
|
||||
@ -1175,7 +1202,9 @@ class MusicTable(QTableView):
|
||||
"""
|
||||
# update the filepath
|
||||
if not filepath:
|
||||
path = self.current_song_qmodel_index.siblingAtColumn(self.headers.db_list.index("filepath")).data()
|
||||
path = self.current_song_qmodel_index.siblingAtColumn(
|
||||
self.headers.db_list.index("filepath")
|
||||
).data()
|
||||
self.current_song_filepath: str = path
|
||||
else:
|
||||
self.current_song_filepath = filepath
|
||||
@ -1234,7 +1263,6 @@ class MusicTable(QTableView):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
# QT Roles
|
||||
|
||||
# In Qt, roles are used to specify different aspects or types of data associated with each item in a model. The roles are defined in the Qt.ItemDataRole enum. The three roles you asked about - DisplayRole, EditRole, and UserRole - are particularly important. Let's break them down:
|
||||
|
||||
@ -43,7 +43,7 @@ class PlaylistsPane(QTreeWidget):
|
||||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.showContextMenu)
|
||||
self.currentItemChanged.connect(self.playlist_clicked)
|
||||
self.playlist_db_id_choice: int | None = None
|
||||
self.playlist_db_id_choice: int = 0
|
||||
font: QFont = QFont()
|
||||
font.setPointSize(14)
|
||||
font.setBold(False)
|
||||
@ -62,6 +62,8 @@ class PlaylistsPane(QTreeWidget):
|
||||
"SELECT id, name FROM playlist ORDER BY date_created DESC;", ()
|
||||
)
|
||||
# debug(f'PlaylistsPane: | playlists = {playlists}')
|
||||
branch = PlaylistWidgetItem(self, -1, "newly added")
|
||||
self._playlists_root.addChild(branch)
|
||||
for playlist in playlists:
|
||||
branch = PlaylistWidgetItem(self, playlist[0], playlist[1])
|
||||
self._playlists_root.addChild(branch)
|
||||
@ -71,7 +73,7 @@ class PlaylistsPane(QTreeWidget):
|
||||
def showContextMenu(self, position: QPoint):
|
||||
"""Right-click context menu"""
|
||||
menu = QMenu(self)
|
||||
if self.playlist_db_id_choice is not None:
|
||||
if self.playlist_db_id_choice > 0:
|
||||
# only allow delete/rename non-root nodes
|
||||
rename_action = QAction("Rename", self)
|
||||
delete_action = QAction("Delete", self)
|
||||
@ -135,20 +137,23 @@ class PlaylistsPane(QTreeWidget):
|
||||
if self.qapp:
|
||||
with DBA.DBAccess() as db:
|
||||
db.execute(
|
||||
"DELETE FROM playlist WHERE id = ?;", (self.playlist_db_id_choice,)
|
||||
"DELETE FROM playlist WHERE id = ?;",
|
||||
(self.playlist_db_id_choice,),
|
||||
)
|
||||
# reload
|
||||
worker = Worker(self.reload_playlists)
|
||||
threadpool = self.qapp.threadpool
|
||||
threadpool.start(worker)
|
||||
else:
|
||||
error_dialog = ErrorDialog("Main application [qapp] not loaded - aborting operation")
|
||||
error_dialog = ErrorDialog(
|
||||
"Main application [qapp] not loaded - aborting operation"
|
||||
)
|
||||
error_dialog.exec()
|
||||
|
||||
def playlist_clicked(self, item):
|
||||
"""Specific playlist pane index was clicked"""
|
||||
if item == self._playlists_root or item == self._library_root:
|
||||
self.playlist_db_id_choice = None
|
||||
self.playlist_db_id_choice = 0
|
||||
# self.all_songs_selected()
|
||||
self.playlistChoiceSignal.emit(0)
|
||||
elif isinstance(item, PlaylistWidgetItem):
|
||||
|
||||
118
main.py
118
main.py
@ -7,8 +7,10 @@ import qdarktheme
|
||||
from PyQt5.QtGui import QFontDatabase
|
||||
from PyQt5 import QtCore
|
||||
from subprocess import run
|
||||
|
||||
# from pyqtgraph import mkBrush
|
||||
from mutagen.id3 import ID3
|
||||
|
||||
# from mutagen.id3._frames import APIC
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
@ -49,7 +51,7 @@ from utils import (
|
||||
set_album_art,
|
||||
id3_remap,
|
||||
get_album_art,
|
||||
Worker
|
||||
Worker,
|
||||
)
|
||||
from components import (
|
||||
MediaPlayer,
|
||||
@ -59,7 +61,7 @@ from components import (
|
||||
CreatePlaylistWindow,
|
||||
ExportPlaylistWindow,
|
||||
HeaderTags2,
|
||||
DebugWindow
|
||||
DebugWindow,
|
||||
)
|
||||
from utils.export_playlist_by_id import export_playlist_by_id
|
||||
|
||||
@ -100,13 +102,17 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
|
||||
# widget bits
|
||||
self.tableView: MusicTable
|
||||
self.tableView.saved_column_ratios: list[str] = str(self.config["table"]["column_ratios"]).split(",") # type: ignore
|
||||
self.tableView.saved_column_ratios: list[str] = str(
|
||||
self.config["table"]["column_ratios"]
|
||||
).split(",") # type: ignore
|
||||
self.album_art_scene: QGraphicsScene = QGraphicsScene()
|
||||
self.player: QMediaPlayer = MediaPlayer()
|
||||
# set index on choose song
|
||||
# index is the model2's row number? i guess?
|
||||
self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data
|
||||
self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player, self.probe, self.PlotWidget)
|
||||
self.audio_visualizer: AudioVisualizer = AudioVisualizer(
|
||||
self.player, self.probe, self.PlotWidget
|
||||
)
|
||||
self.timer: QTimer = QTimer(parent=self) # for playback slider and such
|
||||
|
||||
# Button styles
|
||||
@ -147,10 +153,14 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
self.albumGraphicsView.setFixedSize(250, 250)
|
||||
|
||||
# Connections
|
||||
self.playbackSlider.sliderReleased.connect(lambda: self.player.setPosition(self.playbackSlider.value())) # sliderReleased works better than sliderMoved
|
||||
self.playbackSlider.sliderReleased.connect(
|
||||
lambda: self.player.setPosition(self.playbackSlider.value())
|
||||
) # sliderReleased works better than sliderMoved
|
||||
self.volumeSlider.sliderMoved[int].connect(lambda: self.on_volume_changed())
|
||||
self.speedSlider.sliderMoved.connect(lambda: self.on_speed_changed(self.speedSlider.value()))
|
||||
#self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1))
|
||||
self.speedSlider.sliderMoved.connect(
|
||||
lambda: self.on_speed_changed(self.speedSlider.value())
|
||||
)
|
||||
# self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1))
|
||||
self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause
|
||||
self.previousButton.clicked.connect(self.on_prev_clicked)
|
||||
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
|
||||
@ -165,7 +175,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
self.actionFontListing.triggered.connect(self.open_font_listing)
|
||||
QFontDatabase().families()
|
||||
|
||||
|
||||
# QUICK ACTIONS MENU
|
||||
self.actionScanLibraries.triggered.connect(self.scan_libraries)
|
||||
self.actionDeleteDatabase.triggered.connect(self.delete_database)
|
||||
@ -182,21 +191,30 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
self.searchLineEdit.textTypedSignal.connect(self.handle_search_box_text)
|
||||
# tableView
|
||||
self.tableView.playSignal.connect(self.play_audio_file)
|
||||
self.tableView.playPauseSignal.connect(self.on_play_clicked) # Spacebar toggle play/pause signal
|
||||
self.tableView.playPauseSignal.connect(
|
||||
self.on_play_clicked
|
||||
) # Spacebar toggle play/pause signal
|
||||
self.tableView.handleProgressSignal.connect(self.handle_progress)
|
||||
self.tableView.searchBoxSignal.connect(self.handle_search_box_visibility)
|
||||
self.tableView.playlistStatsSignal.connect(self.set_permanent_status_bar_message)
|
||||
self.tableView.playlistStatsSignal.connect(
|
||||
self.set_permanent_status_bar_message
|
||||
)
|
||||
self.tableView.load_music_table()
|
||||
self.player.playlistNextSignal.connect(self.on_next_clicked)
|
||||
|
||||
# playlistTreeView
|
||||
self.playlistTreeView.playlistChoiceSignal.connect(self.tableView.load_music_table)
|
||||
self.playlistTreeView.playlistChoiceSignal.connect(
|
||||
self.tableView.load_music_table
|
||||
)
|
||||
self.playlistTreeView.allSongsSignal.connect(self.tableView.load_music_table)
|
||||
|
||||
# albumGraphicsView
|
||||
self.albumGraphicsView.albumArtDropped.connect(self.set_album_art_for_selected_songs)
|
||||
self.albumGraphicsView.albumArtDeleted.connect(self.delete_album_art_for_current_song)
|
||||
|
||||
self.albumGraphicsView.albumArtDropped.connect(
|
||||
self.set_album_art_for_selected_songs
|
||||
)
|
||||
self.albumGraphicsView.albumArtDeleted.connect(
|
||||
self.delete_album_art_for_current_song
|
||||
)
|
||||
|
||||
def nothing(self):
|
||||
pass
|
||||
@ -216,8 +234,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
def closeEvent(self, a0: QCloseEvent | None) -> None:
|
||||
"""Save settings when closing the application"""
|
||||
self.config["settings"]["volume"] = str(self.current_volume)
|
||||
self.config["settings"]["window_size"] = (str(self.width()) + "," + str(self.height()))
|
||||
self.config['table']['column_ratios'] = ",".join(self.tableView.get_current_header_width_ratios())
|
||||
self.config["settings"]["window_size"] = (
|
||||
str(self.width()) + "," + str(self.height())
|
||||
)
|
||||
self.config["table"]["column_ratios"] = ",".join(
|
||||
self.tableView.get_current_header_width_ratios()
|
||||
)
|
||||
self.config["table"]["sort_order"] = self.tableView.sort_config
|
||||
# Save the config
|
||||
try:
|
||||
@ -225,10 +247,13 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
self.config.write(configfile)
|
||||
except Exception as e:
|
||||
debug(f"wtf man {e}")
|
||||
debug("Done writing config")
|
||||
# auto export any playlists that want it
|
||||
try:
|
||||
with DBA.DBAccess() as db:
|
||||
result = db.query('SELECT id FROM playlist WHERE auto_export = true;', ())
|
||||
result = db.query(
|
||||
"SELECT id FROM playlist WHERE auto_export = true;", ()
|
||||
)
|
||||
ids = [id[0] for id in result]
|
||||
for id in ids:
|
||||
export_playlist_by_id(id)
|
||||
@ -298,8 +323,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
row: int = index.row()
|
||||
prev_row: int = row - 1
|
||||
prev_index: QModelIndex = self.tableView.proxymodel.index(prev_row, index.column())
|
||||
prev_filepath = prev_index.siblingAtColumn(self.headers.db_list.index("filepath")).data()
|
||||
prev_index: QModelIndex = self.tableView.proxymodel.index(
|
||||
prev_row, index.column()
|
||||
)
|
||||
prev_filepath = prev_index.siblingAtColumn(
|
||||
self.headers.db_list.index("filepath")
|
||||
).data()
|
||||
if prev_filepath is None:
|
||||
return
|
||||
|
||||
@ -318,8 +347,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
row: int = index.row()
|
||||
next_row: int = row + 1
|
||||
next_index: QModelIndex = self.tableView.proxymodel.index(next_row, index.column())
|
||||
next_filepath = next_index.siblingAtColumn(self.headers.db_list.index("filepath")).data()
|
||||
next_index: QModelIndex = self.tableView.proxymodel.index(
|
||||
next_row, index.column()
|
||||
)
|
||||
next_filepath = next_index.siblingAtColumn(
|
||||
self.headers.db_list.index("filepath")
|
||||
).data()
|
||||
if next_filepath is None:
|
||||
return
|
||||
self.play_audio_file(next_filepath)
|
||||
@ -368,8 +401,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
def load_config(self) -> None:
|
||||
"""does what it says"""
|
||||
cfg_file = (
|
||||
Path(user_config_dir(appname="musicpom",
|
||||
appauthor="billypom")) / "config.ini"
|
||||
Path(user_config_dir(appname="musicpom", appauthor="billypom"))
|
||||
/ "config.ini"
|
||||
)
|
||||
self.config.read(cfg_file)
|
||||
debug("load_config()")
|
||||
@ -447,7 +480,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
for song in selected_songs:
|
||||
debug(
|
||||
f"main.py set_album_art_for_selected_songs() | updating album art for {
|
||||
song}"
|
||||
song
|
||||
}"
|
||||
)
|
||||
set_album_art(song, album_art_path)
|
||||
|
||||
@ -474,8 +508,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
self.playbackSlider.setMaximum(self.player.duration())
|
||||
slider_position = self.player.position()
|
||||
self.playbackSlider.setValue(slider_position)
|
||||
current_minutes, current_seconds = divmod(
|
||||
slider_position / 1000, 60)
|
||||
current_minutes, current_seconds = divmod(slider_position / 1000, 60)
|
||||
duration_minutes, duration_seconds = divmod(
|
||||
self.player.duration() / 1000, 60
|
||||
)
|
||||
@ -494,6 +527,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
updates the status bar when progress is emitted
|
||||
"""
|
||||
# breakpoint()
|
||||
self.show_status_bar_message(data)
|
||||
|
||||
# ____________________
|
||||
@ -526,8 +560,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
|
||||
def create_playlist(self) -> None:
|
||||
"""Creates a database record for a playlist, given a name"""
|
||||
window = CreatePlaylistWindow(self.playlistCreatedSignal)
|
||||
window.playlistCreatedSignal.connect(
|
||||
self.add_latest_playlist_to_tree) # type: ignore
|
||||
window.playlistCreatedSignal.connect(self.add_latest_playlist_to_tree) # type: ignore
|
||||
window.exec_()
|
||||
|
||||
def import_playlist(self) -> None:
|
||||
@ -592,22 +625,21 @@ def update_database_file() -> bool:
|
||||
Reads the database file (specified by config file)
|
||||
"""
|
||||
cfg_file = (
|
||||
Path(user_config_dir(appname="musicpom",
|
||||
appauthor="billypom")) / "config.ini"
|
||||
Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini"
|
||||
)
|
||||
cfg_path = str(Path(user_config_dir(
|
||||
appname="musicpom", appauthor="billypom")))
|
||||
cfg_path = str(Path(user_config_dir(appname="musicpom", appauthor="billypom")))
|
||||
config = ConfigParser()
|
||||
config.read(cfg_file)
|
||||
db_filepath: str
|
||||
db_filepath= config.get("settings", "db")
|
||||
db_filepath = config.get("settings", "db")
|
||||
|
||||
# If the database location isnt set at the config location, move it
|
||||
if not db_filepath.startswith(cfg_path):
|
||||
new_path = f"{cfg_path}/{db_filepath}"
|
||||
debug(
|
||||
f"Set new config [db] database path: \n> Current: {
|
||||
db_filepath}\n> New:{new_path}"
|
||||
f"Set new config [db] database path: \n> Current: {db_filepath}\n> New:{
|
||||
new_path
|
||||
}"
|
||||
)
|
||||
config["settings"]["db"] = new_path
|
||||
# Save the config
|
||||
@ -641,11 +673,9 @@ def update_config_file() -> ConfigParser:
|
||||
If the user config file is not up to date, update it with examples from sample config
|
||||
"""
|
||||
cfg_file = (
|
||||
Path(user_config_dir(appname="musicpom",
|
||||
appauthor="billypom")) / "config.ini"
|
||||
Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini"
|
||||
)
|
||||
cfg_path = str(Path(user_config_dir(
|
||||
appname="musicpom", appauthor="billypom")))
|
||||
cfg_path = str(Path(user_config_dir(appname="musicpom", appauthor="billypom")))
|
||||
|
||||
# If config path doesn't exist, create it
|
||||
if not os.path.exists(cfg_path):
|
||||
@ -693,11 +723,11 @@ if __name__ == "__main__":
|
||||
format="{%(filename)s:%(lineno)d} %(levelname)s - %(message)s",
|
||||
handlers=handlers,
|
||||
)
|
||||
debug('--------- musicpom debug started')
|
||||
debug('--------------------------------')
|
||||
debug("--------- musicpom debug started")
|
||||
debug("--------------------------------")
|
||||
debug(handlers)
|
||||
debug('--------------------------------')
|
||||
debug('--------------------------------')
|
||||
debug("--------------------------------")
|
||||
debug("--------------------------------")
|
||||
# Initialization
|
||||
config: ConfigParser = update_config_file()
|
||||
if not update_database_file():
|
||||
@ -713,7 +743,7 @@ if __name__ == "__main__":
|
||||
try:
|
||||
# Dark theme >:3
|
||||
qdarktheme.setup_theme()
|
||||
#qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
|
||||
# qdarktheme.setup_theme("auto") # this is supposed to work but doesnt
|
||||
except Exception:
|
||||
pass
|
||||
# Show the UI
|
||||
|
||||
@ -4,7 +4,7 @@ from .initialize_db import initialize_db
|
||||
from .safe_get import safe_get
|
||||
from .get_album_art import get_album_art
|
||||
from .get_tags import get_tags, id3_remap
|
||||
from .get_reorganize_vars import get_reorganize_vars
|
||||
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_database import batch_delete_filepaths_from_database
|
||||
|
||||
@ -1,26 +1,31 @@
|
||||
from ast import parse
|
||||
import os
|
||||
from PyQt5.QtCore import QThreadPool
|
||||
import DBA
|
||||
import logging
|
||||
from utils import get_reorganize_vars, Worker
|
||||
from utils import get_reorganize_vars, Worker, parse_artist_album
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
|
||||
def export_playlist_by_id(playlist_db_id: int) -> bool:
|
||||
"""
|
||||
Exports a playlist to its defined auto_export_path, by database ID
|
||||
"""
|
||||
logging.debug(f"Exporting playlist id: {playlist_db_id}")
|
||||
threadpool = QThreadPool()
|
||||
try:
|
||||
with DBA.DBAccess() as db:
|
||||
result = db.query('''
|
||||
result = db.query(
|
||||
"""
|
||||
SELECT auto_export_path, path_prefix FROM playlist WHERE id = ?
|
||||
''', (playlist_db_id,)
|
||||
)
|
||||
""",
|
||||
(playlist_db_id,),
|
||||
)
|
||||
auto_export_path = result[0][0]
|
||||
path_prefix = result[0][1]
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"export_playlist_by_id.py | Could not contact database: {e}"
|
||||
)
|
||||
logging.error(f"export_playlist_by_id.py | Could not contact database: {e}")
|
||||
return False
|
||||
|
||||
# If the prefix is null, then use an empty string
|
||||
@ -40,7 +45,7 @@ def export_playlist_by_id(playlist_db_id: int) -> bool:
|
||||
WHERE sp.playlist_id = ?;""",
|
||||
(playlist_db_id,),
|
||||
)
|
||||
db_paths = [path[0] for path in data]
|
||||
db_paths = [Path(path[0]) for path in data]
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"export_playlist_by_id.py | Could not retrieve records from playlist: {e}"
|
||||
@ -50,13 +55,11 @@ def export_playlist_by_id(playlist_db_id: int) -> bool:
|
||||
# Gather playlist song paths
|
||||
write_paths = []
|
||||
# Relative paths
|
||||
logging.debug("Creating relative paths...")
|
||||
for song in db_paths:
|
||||
artist, album = get_reorganize_vars(song)
|
||||
write_path = os.path.join(
|
||||
path_prefix, artist, album, song.split("/")[-1] + "\n"
|
||||
)
|
||||
write_paths.append(str(write_path))
|
||||
|
||||
artist, album = parse_artist_album(song)
|
||||
write_path = Path(path_prefix) / artist / album / song.name
|
||||
write_paths.append(str(write_path) + "\n")
|
||||
|
||||
worker = Worker(write_to_playlist_file, write_paths, auto_export_path)
|
||||
# worker.signals.signal_finished.connect(None)
|
||||
@ -64,10 +67,15 @@ def export_playlist_by_id(playlist_db_id: int) -> bool:
|
||||
threadpool.start(worker)
|
||||
return True
|
||||
|
||||
def write_to_playlist_file(paths: list[str], outfile: str, progress_callback=None) -> None:
|
||||
|
||||
def write_to_playlist_file(
|
||||
paths: list[str], outfile: str, progress_callback=None
|
||||
) -> None:
|
||||
"""
|
||||
Writes a list of strings to a m3u file
|
||||
"""
|
||||
os.makedirs(os.path.dirname(outfile), exist_ok=True)
|
||||
logging.debug("Writing paths to m3u file")
|
||||
with open(outfile, "w") as f:
|
||||
f.writelines(paths)
|
||||
logging.debug("Done writing m3u files")
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from mutagen.id3 import ID3
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_reorganize_vars(filepath: str) -> tuple[str, str]:
|
||||
def get_reorganize_vars(filepath: Path) -> tuple[str, str]:
|
||||
"""
|
||||
Takes in a path to an audio file
|
||||
returns the (artist, album) as a tuple of strings
|
||||
@ -9,6 +10,7 @@ def get_reorganize_vars(filepath: str) -> tuple[str, str]:
|
||||
if no artist or album or ID3 tags at all are found,
|
||||
function will return ("Unknown Artist", "Unknown Album")
|
||||
"""
|
||||
# logging.debug(f"getting reorganize vars for {filepath}")
|
||||
# TODO: fix this func. id3_remap(get_tags())
|
||||
# or is what i have less memory so more better? :shrug:
|
||||
audio = ID3(filepath)
|
||||
@ -27,3 +29,78 @@ def get_reorganize_vars(filepath: str) -> tuple[str, str]:
|
||||
album = "Unknown Album"
|
||||
|
||||
return artist, album
|
||||
|
||||
|
||||
def synchsafe_to_int(b):
|
||||
"""Convert 4 synchsafe bytes to int (ID3v2.4)."""
|
||||
return (b[0] << 21) | (b[1] << 14) | (b[2] << 7) | b[3]
|
||||
|
||||
|
||||
def decode_text_frame(data):
|
||||
"""Decode ID3 text frame payload."""
|
||||
if not data:
|
||||
return None
|
||||
|
||||
encoding_byte = data[0]
|
||||
text_bytes = data[1:]
|
||||
|
||||
if encoding_byte == 0:
|
||||
return text_bytes.decode("iso-8859-1", errors="replace").rstrip("\x00")
|
||||
elif encoding_byte == 1:
|
||||
return text_bytes.decode("utf-16", errors="replace").rstrip("\x00")
|
||||
elif encoding_byte == 2:
|
||||
return text_bytes.decode("utf-16-be", errors="replace").rstrip("\x00")
|
||||
elif encoding_byte == 3:
|
||||
return text_bytes.decode("utf-8", errors="replace").rstrip("\x00")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def parse_artist_album(filepath) -> tuple[str, str]:
|
||||
with open(filepath, "rb") as f:
|
||||
header = f.read(10)
|
||||
|
||||
if len(header) < 10 or header[0:3] != b"ID3":
|
||||
raise ValueError("No ID3v2 tag found")
|
||||
|
||||
version_major = header[3]
|
||||
tag_size = synchsafe_to_int(header[6:10])
|
||||
|
||||
artist = None
|
||||
album = None
|
||||
|
||||
start_pos = f.tell()
|
||||
|
||||
while f.tell() - start_pos < tag_size:
|
||||
frame_header = f.read(10)
|
||||
if len(frame_header) < 10:
|
||||
break
|
||||
|
||||
frame_id = frame_header[0:4].decode("ascii", errors="replace")
|
||||
|
||||
# Stop at padding
|
||||
if frame_id.strip("\x00") == "":
|
||||
break
|
||||
|
||||
if version_major == 4:
|
||||
frame_size = synchsafe_to_int(frame_header[4:8])
|
||||
else:
|
||||
frame_size = int.from_bytes(frame_header[4:8], "big")
|
||||
|
||||
# Skip flags (bytes 8–9 already included in header)
|
||||
frame_data = f.read(frame_size)
|
||||
|
||||
if frame_id == "TPE1":
|
||||
artist = decode_text_frame(frame_data)
|
||||
elif frame_id == "TALB":
|
||||
album = decode_text_frame(frame_data)
|
||||
|
||||
if artist and album:
|
||||
break
|
||||
|
||||
if not artist:
|
||||
artist = "Unknown Arist"
|
||||
if not album:
|
||||
album = "Unknown Album"
|
||||
|
||||
return artist, album
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user