Compare commits

..

12 Commits

Author SHA1 Message Date
Keith Edmunds
125a44c645 Add 'this month then' note colour 2021-10-16 10:33:32 +01:00
Keith Edmunds
a72a86cfcc Don't prompt for duplicate track on a rescan
Fixes #87
2021-10-15 15:02:25 +01:00
Keith Edmunds
1a16b1022d Implement tab close buttons
Fixes #81
2021-09-29 21:29:20 +01:00
Keith Edmunds
69fb10fcd9 Make database update check cron-friendly.
Fixes #85
2021-09-29 20:55:39 +01:00
Keith Edmunds
1a4f842f1f Set last played time when playing track
Fixes #83
2021-09-26 08:47:00 +01:00
Keith Edmunds
69dd0235a0 Improve note colouring
- Make case insensitive
 - If not starts with key, it's a match

Fixes #71
2021-09-25 22:33:17 +01:00
Keith Edmunds
ab858a62fd Fix moving tracks with Wikipedia tabs open
Fixes #77
2021-09-25 22:22:34 +01:00
Keith Edmunds
01b531aabf Scroll to show moved tracks on drag and drop
Fixes #75
2021-09-24 15:10:17 +01:00
Keith Edmunds
6ccfae0ab1 Add note colouring by keyword
Fixes #71
2021-09-24 14:58:35 +01:00
Keith Edmunds
9cf9ef9a59 Add ^T shortcut to add note
Fixes #69
2021-09-24 14:43:33 +01:00
Keith Edmunds
21fe8fff83 Update track.lastplayed field
Fixes #78
2021-09-24 08:05:01 +01:00
Keith Edmunds
780b053219 Check for duplicate title on import
Fixes #80
2021-09-23 18:07:28 +01:00
7 changed files with 93 additions and 26 deletions

View File

@ -38,6 +38,13 @@ class Config(object):
MILLISECOND_SIGFIGS = 0 MILLISECOND_SIGFIGS = 0
MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev" # noqa E501 MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev" # noqa E501
NORMALISE_ON_IMPORT = True NORMALISE_ON_IMPORT = True
NOTE_COLOURS = {
'track': "#ffff00",
'request': "#7cf000",
'wrap': "#fffacd",
'this month then': "#c256c2",
'story': "#dda0dd",
}
ROOT = os.environ.get('ROOT') or "/home/kae/music" ROOT = os.environ.get('ROOT') or "/home/kae/music"
TESTMODE = True TESTMODE = True
TIMER_MS = 500 TIMER_MS = 500

View File

@ -106,7 +106,7 @@ class Playdates(Base):
pd.lastplayed = datetime.now() pd.lastplayed = datetime.now()
pd.track_id = track.id pd.track_id = track.id
session.add(pd) session.add(pd)
track.update_lastplayed() track.update_lastplayed(session, track.id)
session.commit() session.commit()
@staticmethod @staticmethod
@ -596,8 +596,11 @@ class Tracks(Base):
return session.query(Tracks).filter( return session.query(Tracks).filter(
Tracks.id == id).one() Tracks.id == id).one()
def update_lastplayed(self): @staticmethod
self.lastplayed = datetime.now() def update_lastplayed(session, track_id):
track = session.query(Tracks).filter(Tracks.id == track_id).one()
track.lastplayed = datetime.now()
session.commit()
@staticmethod @staticmethod
def update_artist(session, track_id, artist): def update_artist(session, track_id, artist):

View File

@ -166,6 +166,7 @@ class Window(QMainWindow, Ui_MainWindow):
def connect_signals_slots(self): def connect_signals_slots(self):
self.actionAdd_file.triggered.connect(self.add_file) self.actionAdd_file.triggered.connect(self.add_file)
self.actionAdd_note.triggered.connect(self.insert_note)
self.action_Clear_selection.triggered.connect(self.clear_selection) self.action_Clear_selection.triggered.connect(self.clear_selection)
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
self.actionExport_playlist.triggered.connect(self.export_playlist_tab) self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
@ -196,6 +197,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnSongInfo.clicked.connect(self.song_info_search) self.btnSongInfo.clicked.connect(self.song_info_search)
self.btnStop.clicked.connect(self.stop) self.btnStop.clicked.connect(self.stop)
self.spnVolume.valueChanged.connect(self.change_volume) self.spnVolume.valueChanged.connect(self.change_volume)
self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
@ -227,6 +229,15 @@ class Window(QMainWindow, Ui_MainWindow):
index = self.tabPlaylist.currentIndex() index = self.tabPlaylist.currentIndex()
self.tabPlaylist.removeTab(index) self.tabPlaylist.removeTab(index)
def close_tab(self, index):
if self.tabPlaylist.widget(index) == self.current_track_playlist_tab:
self.statusbar.showMessage("Can't close current track playlist",
5000)
elif self.tabPlaylist.widget(index) == self.next_track_playlist_tab:
self.statusbar.showMessage("Can't close next track playlist", 5000)
else:
self.tabPlaylist.removeTab(index)
def create_note(self, session, text): def create_note(self, session, text):
""" """
Create note Create note
@ -419,6 +430,9 @@ class Window(QMainWindow, Ui_MainWindow):
# the playlistis opened. # the playlistis opened.
destination_visible_playlist_tab = None destination_visible_playlist_tab = None
for tab in range(self.tabPlaylist.count()): for tab in range(self.tabPlaylist.count()):
# Non-playlist tabs won't have ids
if not hasattr(self.tabPlaylist.widget(tab), 'id'):
continue
if self.tabPlaylist.widget(tab).id == dlg.plid: if self.tabPlaylist.widget(tab).id == dlg.plid:
destination_visible_playlist_tab = ( destination_visible_playlist_tab = (
self.tabPlaylist.widget(tab)) self.tabPlaylist.widget(tab))
@ -511,7 +525,6 @@ class Window(QMainWindow, Ui_MainWindow):
QColor(Config.COLOUR_NEXT_TAB)) QColor(Config.COLOUR_NEXT_TAB))
# Tell database to record it as played # Tell database to record it as played
self.current_track.update_lastplayed()
Playdates.add_playdate(session, self.current_track) Playdates.add_playdate(session, self.current_track)
self.disable_play_next_controls() self.disable_play_next_controls()

View File

@ -141,6 +141,8 @@ class PlaylistTab(QTableWidget):
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN) self.NOTE_COL_SPAN)
# Scroll to drop zone
self.scrollToItem(self.item(row, 1))
super().dropEvent(event) super().dropEvent(event)
DEBUG( DEBUG(
@ -1028,8 +1030,13 @@ class PlaylistTab(QTableWidget):
if row_time: if row_time:
next_start_time = row_time next_start_time = row_time
# Set colour # Set colour
note_colour = Config.COLOUR_NOTES_PLAYLIST
note_text = self.item(row, self.COL_TITLE).text()
for colour_token in Config.NOTE_COLOURS.keys():
if note_text.lower().startswith(colour_token.lower()):
note_colour = Config.NOTE_COLOURS[colour_token]
self._set_row_colour( self._set_row_colour(
row, QColor(Config.COLOUR_NOTES_PLAYLIST) row, QColor(note_colour)
) )
self._set_row_bold(row) self._set_row_bold(row)
@ -1044,6 +1051,10 @@ class PlaylistTab(QTableWidget):
# Set start time # Set start time
self._set_row_start_time( self._set_row_start_time(
row, self.current_track_start_time) row, self.current_track_start_time)
last_played_str = get_relative_date(
self.current_track_start_time)
self.item(row, self.COL_LAST_PLAYED).setText(
last_played_str)
# Calculate next_start_time # Calculate next_start_time
next_start_time = self._calculate_next_start_time( next_start_time = self._calculate_next_start_time(
session, row, self.current_track_start_time) session, row, self.current_track_start_time)

View File

@ -14,11 +14,13 @@ from mutagen.mp3 import MP3
from pydub import AudioSegment, effects from pydub import AudioSegment, effects
from tinytag import TinyTag from tinytag import TinyTag
# Globals (I know)
messages = []
def main(): def main():
"Main loop" "Main loop"
INFO("Starting") DEBUG("Starting")
# Parse command line # Parse command line
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
@ -36,25 +38,25 @@ def main():
# Run as required # Run as required
if args.update: if args.update:
INFO("Updating database") DEBUG("Updating database")
with Session() as session: with Session() as session:
update_db(session) update_db(session)
elif args.full_update: elif args.full_update:
INFO("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: elif args.fname:
fname = os.path.realpath(args.fname) fname = os.path.realpath(args.fname)
with Session() as session: with Session() as session:
create_track_from_file(session, fname, verbose=True) create_track_from_file(session, fname, interactive=True)
else: else:
INFO("No action specified") INFO("No action specified")
INFO("Finished") DEBUG("Finished")
def create_track_from_file(session, path, verbose=False): def create_track_from_file(session, path, 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.
@ -62,23 +64,35 @@ def create_track_from_file(session, path, verbose=False):
Return track. Return track.
""" """
if verbose: if interactive:
str = f"Importing {path}" str = f"Importing {path}"
INFO(str) INFO(str)
INFO("-" * len(str)) INFO("-" * len(str))
track = Tracks.get_or_create(session, path)
if verbose:
INFO("Get track info...") INFO("Get track info...")
t = get_music_info(path) t = get_music_info(path)
track.title = t['title'] title = t['title']
track.artist = t['artist'] artist = t['artist']
if verbose: if interactive:
INFO(f" Title: \"{track.title}\"") INFO(f" Title: \"{title}\"")
INFO(f" Artist: \"{track.artist}\"") INFO(f" Artist: \"{artist}\"")
# Check for duplicate
tracks = Tracks.search_titles(session, title)
if interactive and 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.title = title
track.artist = artist
track.duration = int(round( track.duration = int(round(
t['duration'], Config.MILLISECOND_SIGFIGS) * 1000) t['duration'], Config.MILLISECOND_SIGFIGS) * 1000)
if verbose: if interactive:
INFO("Parse for start, fade and silence...") INFO("Parse for start, fade and silence...")
audio = get_audio_segment(path) audio = get_audio_segment(path)
track.start_gap = leading_silence(audio) track.start_gap = leading_silence(audio)
@ -90,7 +104,7 @@ def create_track_from_file(session, path, verbose=False):
session.commit() session.commit()
if Config.NORMALISE_ON_IMPORT: if Config.NORMALISE_ON_IMPORT:
if verbose: if interactive:
INFO("Normalise...") INFO("Normalise...")
# Check type # Check type
ftype = os.path.splitext(path)[1][1:] ftype = os.path.splitext(path)[1][1:]
@ -317,8 +331,7 @@ def update_db(session):
# is filename in database? # is filename in database?
track = Tracks.get_track_from_filename(session, os.path.basename(path)) track = Tracks.get_track_from_filename(session, os.path.basename(path))
if not track: if not track:
INFO(f"songdb.update_db: Adding to database: {path}") messages.append(f"Track missing from database: {path}")
create_track_from_file(session, path)
else: else:
# Check track info matches found track # Check track info matches found track
t = get_music_info(path) t = get_music_info(path)
@ -333,7 +346,7 @@ def update_db(session):
for path in list(db_paths - os_paths): for path in list(db_paths - os_paths):
# Manage tracks listed in database but where path is invalid # Manage tracks listed in database but where path is invalid
track = Tracks.get_track_from_path(session, path) track = Tracks.get_track_from_path(session, path)
INFO(f"songdb.update_db(): remove from database: {path=} {track=}") messages.append(f"Remove from database: {path=} {track=}")
# Remove references from Playdates # Remove references from Playdates
Playdates.remove_track(session, track.id) Playdates.remove_track(session, track.id)
@ -352,6 +365,12 @@ def update_db(session):
# Remove Track entry pointing to invalid path # Remove Track entry pointing to invalid path
Tracks.remove_path(session, path) Tracks.remove_path(session, path)
# Output messages (so if running via cron, these will get sent to
# user)
if messages:
print("Messages")
print("\n".join(messages))
def update_meta(session, track, artist=None, title=None): def update_meta(session, track, artist=None, title=None):
""" """

View File

@ -468,7 +468,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="tabsClosable"> <property name="tabsClosable">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="movable"> <property name="movable">
<bool>true</bool> <bool>true</bool>
@ -770,6 +770,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSearch_database"/> <addaction name="actionSearch_database"/>
<addaction name="actionAdd_file"/> <addaction name="actionAdd_file"/>
<addaction name="actionAdd_note"/>
<addaction name="action_Clear_selection"/> <addaction name="action_Clear_selection"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSelect_unplayed_tracks"/> <addaction name="actionSelect_unplayed_tracks"/>
@ -1004,6 +1005,14 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>Select unplayed tracks</string> <string>Select unplayed tracks</string>
</property> </property>
</action> </action>
<action name="actionAdd_note">
<property name="text">
<string>Add note...</string>
</property>
<property name="shortcut">
<string>Ctrl+T</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -208,7 +208,7 @@ class Ui_MainWindow(object):
self.gridLayout_3.addWidget(self.frame_5, 1, 0, 1, 1) self.gridLayout_3.addWidget(self.frame_5, 1, 0, 1, 1)
self.tabPlaylist = QtWidgets.QTabWidget(self.centralwidget) self.tabPlaylist = QtWidgets.QTabWidget(self.centralwidget)
self.tabPlaylist.setDocumentMode(False) self.tabPlaylist.setDocumentMode(False)
self.tabPlaylist.setTabsClosable(False) self.tabPlaylist.setTabsClosable(True)
self.tabPlaylist.setMovable(True) self.tabPlaylist.setMovable(True)
self.tabPlaylist.setObjectName("tabPlaylist") self.tabPlaylist.setObjectName("tabPlaylist")
self.gridLayout_3.addWidget(self.tabPlaylist, 2, 0, 1, 1) self.gridLayout_3.addWidget(self.tabPlaylist, 2, 0, 1, 1)
@ -438,6 +438,8 @@ class Ui_MainWindow(object):
self.actionSelect_played_tracks.setObjectName("actionSelect_played_tracks") self.actionSelect_played_tracks.setObjectName("actionSelect_played_tracks")
self.actionSelect_unplayed_tracks = QtWidgets.QAction(MainWindow) self.actionSelect_unplayed_tracks = QtWidgets.QAction(MainWindow)
self.actionSelect_unplayed_tracks.setObjectName("actionSelect_unplayed_tracks") self.actionSelect_unplayed_tracks.setObjectName("actionSelect_unplayed_tracks")
self.actionAdd_note = QtWidgets.QAction(MainWindow)
self.actionAdd_note.setObjectName("actionAdd_note")
self.menuFile.addAction(self.actionE_xit) self.menuFile.addAction(self.actionE_xit)
self.menuPlaylist.addAction(self.actionNewPlaylist) self.menuPlaylist.addAction(self.actionNewPlaylist)
self.menuPlaylist.addAction(self.actionOpenPlaylist) self.menuPlaylist.addAction(self.actionOpenPlaylist)
@ -447,6 +449,7 @@ class Ui_MainWindow(object):
self.menuPlaylist.addSeparator() self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSearch_database) self.menuPlaylist.addAction(self.actionSearch_database)
self.menuPlaylist.addAction(self.actionAdd_file) self.menuPlaylist.addAction(self.actionAdd_file)
self.menuPlaylist.addAction(self.actionAdd_note)
self.menuPlaylist.addAction(self.action_Clear_selection) self.menuPlaylist.addAction(self.action_Clear_selection)
self.menuPlaylist.addSeparator() self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSelect_unplayed_tracks) self.menuPlaylist.addAction(self.actionSelect_unplayed_tracks)
@ -545,5 +548,7 @@ class Ui_MainWindow(object):
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K")) self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks")) self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks")) self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks"))
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
from musicmuster import ElideLabel from musicmuster import ElideLabel
import icons_rc import icons_rc