Compare commits

...

6 Commits

Author SHA1 Message Date
Keith Edmunds
42092d3d39 Add 'last played' time to track select from database box
Fixes #116
2022-06-04 23:05:39 +01:00
Keith Edmunds
fbe9c2ba94 Fix deleting multiple rows
Also allow mass delete to be cancelled.

Fixes #115
2022-06-04 22:56:38 +01:00
Keith Edmunds
a8395d8c97 Fix background importing and duplicate checking 2022-06-04 22:32:22 +01:00
Keith Edmunds
fc9a10ad52 Tidy up playlist.remove_track 2022-05-02 17:32:29 +01:00
Keith Edmunds
8b644ee236 Clarify comment 2022-05-02 16:09:29 +01:00
Keith Edmunds
c7f7f25af0 Run file import in separate thread 2022-04-19 15:25:15 +01:00
6 changed files with 75 additions and 54 deletions

View File

@ -54,6 +54,7 @@ class Config(object):
NORMALISE_ON_IMPORT = True NORMALISE_ON_IMPORT = True
NOTE_TIME_FORMAT = "%H:%M:%S" NOTE_TIME_FORMAT = "%H:%M:%S"
ROOT = os.environ.get('ROOT') or "/home/kae/music" ROOT = os.environ.get('ROOT') or "/home/kae/music"
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
SCROLL_TOP_MARGIN = 3 SCROLL_TOP_MARGIN = 3
TESTMODE = True TESTMODE = True
TOD_TIME_FORMAT = "%H:%M:%S" TOD_TIME_FORMAT = "%H:%M:%S"

View File

@ -63,12 +63,13 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
tag: TinyTag = TinyTag.get(path) tag: TinyTag = TinyTag.get(path)
return dict( d = dict(
title=tag.title, title=tag.title,
artist=tag.artist, artist=tag.artist,
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000), duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
path=path path=path
) )
return d
def get_relative_date(past_date: datetime, reference_date: datetime = None) \ def get_relative_date(past_date: datetime, reference_date: datetime = None) \

View File

@ -341,13 +341,14 @@ class Playlists(Base):
def remove_track(self, session: Session, row: int) -> None: def remove_track(self, session: Session, row: int) -> None:
DEBUG(f"Playlist.remove_track({self.id=}, {row=})") DEBUG(f"Playlist.remove_track({self.id=}, {row=})")
# Refresh self first (this is necessary when calling remove_track
# multiple times before session.commit())
session.refresh(self)
# Get tracks collection for this playlist # Get tracks collection for this playlist
tracks_collections = self.tracks
# Tracks are a dictionary of tracks keyed on row # Tracks are a dictionary of tracks keyed on row
# number. Remove the relevant row. # number. Remove the relevant row.
del tracks_collections[row] del self.tracks[row]
# Save the new tracks collection # Save the new tracks collection
self.tracks = tracks_collections
session.flush() session.flush()

View File

@ -4,6 +4,7 @@ import argparse
import os.path import os.path
import psutil import psutil
import sys import sys
import threading
import urllib.parse import urllib.parse
import webbrowser import webbrowser
@ -24,6 +25,7 @@ from PyQt5.QtWidgets import (
QLineEdit, QLineEdit,
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QMessageBox,
) )
import dbconfig import dbconfig
@ -420,14 +422,52 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = QFileDialog() dlg = QFileDialog()
dlg.setFileMode(QFileDialog.ExistingFiles) dlg.setFileMode(QFileDialog.ExistingFiles)
dlg.setViewMode(QFileDialog.Detail) dlg.setViewMode(QFileDialog.Detail)
# TODO: remove hardcoded directory dlg.setDirectory(Config.IMPORT_DESTINATION)
dlg.setDirectory(os.path.join(Config.ROOT, "Singles"))
dlg.setNameFilter("Music files (*.flac *.mp3)") dlg.setNameFilter("Music files (*.flac *.mp3)")
if dlg.exec_(): if dlg.exec_():
with Session() as session: with Session() as session:
txt: str = ""
new_tracks = []
for fname in dlg.selectedFiles(): for fname in dlg.selectedFiles():
track = create_track_from_file(session, fname) tags = helpers.get_tags(fname)
new_tracks.append((fname, tags))
title = tags['title']
artist = tags['artist']
possible_matches = Tracks.search_titles(session, title)
if possible_matches:
txt += 'Similar to new track '
txt += f'"{title}" by "{artist} ({fname})":\n\n'
for track in possible_matches:
txt += f' "{track.title}" by {track.artist}'
txt += f' ({track.path})\n'
txt += "\n"
# Check whether to proceed if there were potential matches
if txt:
txt += "Proceed with import?"
result = QMessageBox.question(self,
"Possible duplicates",
txt,
QMessageBox.Ok,
QMessageBox.Cancel
)
if result == QMessageBox.Cancel:
return
# Import in separate thread
thread = threading.Thread(target=self._import_tracks,
args=(new_tracks,))
thread.start()
def _import_tracks(self, tracks: list):
"""
Import passed files. Don't use parent session as that may be invalid
by the time we need it.
"""
with Session() as session:
for (fname, tags) in tracks:
track = create_track_from_file(session, fname, tags=tags)
# Add to playlist on screen # Add to playlist on screen
# If we don't specify "repaint=False", playlist will # If we don't specify "repaint=False", playlist will
# also be saved to database # also be saved to database
@ -979,7 +1019,8 @@ class DbDialog(QDialog):
t = QListWidgetItem() t = QListWidgetItem()
t.setText( t.setText(
f"{track.title} - {track.artist} " f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}]" f"[{helpers.ms_to_mmss(track.duration)}] "
f"({helpers.get_relative_date(track.lastplayed)})"
) )
t.setData(Qt.UserRole, track) t.setData(Qt.UserRole, track)
self.ui.matchList.addItem(t) self.ui.matchList.addItem(t)

View File

@ -1033,7 +1033,7 @@ class PlaylistTab(QTableWidget):
DEBUG("playlist._delete_rows()") DEBUG("playlist._delete_rows()")
rows: List[int] = sorted( selected_rows: List[int] = sorted(
set(item.row() for item in self.selectedItems()) set(item.row() for item in self.selectedItems())
) )
rows_to_delete: List[int] = [] rows_to_delete: List[int] = []
@ -1042,17 +1042,22 @@ class PlaylistTab(QTableWidget):
row_object: Union[Tracks, Notes] row_object: Union[Tracks, Notes]
with Session() as session: with Session() as session:
for row in rows: for row in selected_rows:
title = self.item(row, self.COL_TITLE).text() title = self.item(row, self.COL_TITLE).text()
msg = QMessageBox(self) msg = QMessageBox(self)
msg.setIcon(QMessageBox.Warning) msg.setIcon(QMessageBox.Warning)
msg.setText(f"Delete '{title}'?") msg.setText(f"Delete '{title}'?")
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) msg.setStandardButtons(
msg.setDefaultButton(QMessageBox.Cancel) QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
)
msg.setDefaultButton(QMessageBox.No)
msg.setWindowTitle("Delete row") msg.setWindowTitle("Delete row")
# Store list of rows to delete # Store list of rows to delete
if msg.exec() == QMessageBox.Yes: response = msg.exec()
if response == QMessageBox.Yes:
rows_to_delete.append(row) rows_to_delete.append(row)
elif response == QMessageBox.Cancel:
return
# delete in reverse row order so row numbers don't # delete in reverse row order so row numbers don't
# change # change

View File

@ -38,7 +38,6 @@ def main():
group.add_argument('-f', '--full-update', group.add_argument('-f', '--full-update',
action="store_true", dest="full_update", action="store_true", dest="full_update",
default=False, help="Update database") default=False, help="Update database")
group.add_argument('-i', '--import', dest="fname", help="Input file")
args = p.parse_args() args = p.parse_args()
# Run as required # Run as required
@ -50,18 +49,13 @@ def main():
DEBUG("Full update of database") DEBUG("Full update of database")
with Session() as session: with Session() as session:
full_update_db(session) full_update_db(session)
elif args.fname:
fname = os.path.realpath(args.fname)
with Session() as session:
create_track_from_file(session, fname, interactive=True)
else: else:
INFO("No action specified") INFO("No action specified")
DEBUG("Finished") DEBUG("Finished")
def create_track_from_file(session, path, normalise=None, interactive=False): def create_track_from_file(session, path, normalise=None, tags=None):
""" """
Create track in database from passed path, or update database entry Create track in database from passed path, or update database entry
if path already in database. if path already in database.
@ -69,34 +63,14 @@ def create_track_from_file(session, path, normalise=None, interactive=False):
Return track. Return track.
""" """
if interactive: if not tags:
msg = f"Importing {path}"
INFO(msg)
INFO("-" * len(msg))
INFO("Get track info...")
t = get_tags(path) t = get_tags(path)
title = t['title'] else:
artist = t['artist'] t = tags
if interactive:
INFO(f" Title: \"{title}\"")
INFO(f" Artist: \"{artist}\"")
# Check for duplicate
if interactive:
tracks = Tracks.search_titles(session, title)
if tracks:
print("Found the following possible matches:")
for track in tracks:
print(f'"{track.title}" by {track.artist}')
response = input("Continue [c] or abort [a]?")
if not response:
return
if response[0].lower() not in ['c', 'y']:
return
track = Tracks.get_or_create(session, path) track = Tracks.get_or_create(session, path)
track.title = title track.title = t['title']
track.artist = artist track.artist = t['artist']
if interactive:
INFO("Parse for start, fade and silence...")
audio = get_audio_segment(path) audio = get_audio_segment(path)
track.duration = len(audio) track.duration = len(audio)
track.start_gap = leading_silence(audio) track.start_gap = leading_silence(audio)
@ -108,8 +82,6 @@ def create_track_from_file(session, path, normalise=None, interactive=False):
session.commit() session.commit()
if normalise or normalise is None and Config.NORMALISE_ON_IMPORT: if normalise or normalise is None and Config.NORMALISE_ON_IMPORT:
if interactive:
INFO("Normalise...")
# Check type # Check type
ftype = os.path.splitext(path)[1][1:] ftype = os.path.splitext(path)[1][1:]
if ftype not in ['mp3', 'flac']: if ftype not in ['mp3', 'flac']:
@ -252,7 +224,7 @@ def update_db(session):
for path in list(os_paths - db_paths): for path in list(os_paths - db_paths):
DEBUG(f"utilities.update_db: {path=} not in database") DEBUG(f"utilities.update_db: {path=} not in database")
# is filename in database? # is filename in database with a different path?
track = Tracks.get_by_filename(session, os.path.basename(path)) track = Tracks.get_by_filename(session, os.path.basename(path))
if not track: if not track:
messages.append(f"{path} missing from database: {path}") messages.append(f"{path} missing from database: {path}")