Compare commits

..

No commits in common. "42092d3d392c34350260729d9241162eaaf92b8b" and "e2af6dd7ac18e4d83dd30493e76d6aff857da185" have entirely different histories.

6 changed files with 54 additions and 75 deletions

View File

@ -54,7 +54,6 @@ 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,13 +63,12 @@ def get_tags(path: str) -> Dict[str, Union[str, int]]:
tag: TinyTag = TinyTag.get(path) tag: TinyTag = TinyTag.get(path)
d = dict( return 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,14 +341,13 @@ 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 self.tracks[row] del tracks_collections[row]
# Save the new tracks collection # Save the new tracks collection
self.tracks = tracks_collections
session.flush() session.flush()

View File

@ -4,7 +4,6 @@ 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
@ -25,7 +24,6 @@ from PyQt5.QtWidgets import (
QLineEdit, QLineEdit,
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QMessageBox,
) )
import dbconfig import dbconfig
@ -422,52 +420,14 @@ 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)
dlg.setDirectory(Config.IMPORT_DESTINATION) # TODO: remove hardcoded directory
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():
tags = helpers.get_tags(fname) track = create_track_from_file(session, 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
@ -1019,8 +979,7 @@ 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()")
selected_rows: List[int] = sorted( 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,22 +1042,17 @@ class PlaylistTab(QTableWidget):
row_object: Union[Tracks, Notes] row_object: Union[Tracks, Notes]
with Session() as session: with Session() as session:
for row in selected_rows: for row in 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( msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel msg.setDefaultButton(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
response = msg.exec() if msg.exec() == QMessageBox.Yes:
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,6 +38,7 @@ 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
@ -49,13 +50,18 @@ 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, tags=None): def create_track_from_file(session, path, normalise=None, interactive=False):
""" """
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.
@ -63,14 +69,34 @@ def create_track_from_file(session, path, normalise=None, tags=None):
Return track. Return track.
""" """
if not tags: if interactive:
msg = f"Importing {path}"
INFO(msg)
INFO("-" * len(msg))
INFO("Get track info...")
t = get_tags(path) t = get_tags(path)
else: title = t['title']
t = tags artist = t['artist']
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 = t['title'] track.title = title
track.artist = t['artist'] track.artist = 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)
@ -82,6 +108,8 @@ def create_track_from_file(session, path, normalise=None, tags=None):
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']:
@ -224,7 +252,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 with a different path? # is filename in database?
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}")