Compare commits

...

5 Commits

Author SHA1 Message Date
Keith Edmunds
d4f542cc29 Warn when trying to delete playing or next track 2021-08-15 11:17:09 +01:00
Keith Edmunds
2c9f041838 Show last track in playlist as playing when it is
Fixes #52
2021-08-15 11:06:08 +01:00
Keith Edmunds
90a8209551 Clean up of musicmuster.py 2021-08-15 10:40:28 +01:00
Keith Edmunds
c0752407b9 Handle next track not found consistently
Highlight in red, don't set as next track.
Fixes #51
2021-08-15 10:13:42 +01:00
Keith Edmunds
87fb74b14f Tidy up model.py 2021-08-15 09:21:32 +01:00
5 changed files with 130 additions and 113 deletions

View File

@ -1,4 +1,5 @@
from datetime import datetime, date from datetime import datetime
from PyQt5.QtWidgets import QMessageBox
def get_relative_date(past_date, reference_date=None): def get_relative_date(past_date, reference_date=None):
@ -36,6 +37,12 @@ def get_relative_date(past_date, reference_date=None):
return f"{weeks} {weeks_str}, {days} {days_str} ago" return f"{weeks} {weeks_str}, {days} {days_str} ago"
def show_warning(title, msg):
"Display a warning to user"
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def ms_to_mmss(ms, decimals=0, negative=False): def ms_to_mmss(ms, decimals=0, negative=False):
if not ms: if not ms:
return "-" return "-"

View File

@ -2,7 +2,7 @@
import sqlalchemy import sqlalchemy
from datetime import datetime, time from datetime import datetime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
@ -53,6 +53,8 @@ class Notes(Base):
@staticmethod @staticmethod
def add_note(session, playlist_id, row, text): def add_note(session, playlist_id, row, text):
"Add note"
DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})") DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})")
note = Notes() note = Notes()
note.playlist_id = playlist_id note.playlist_id = playlist_id
@ -64,16 +66,13 @@ class Notes(Base):
@staticmethod @staticmethod
def delete_note(session, id): def delete_note(session, id):
"Delete note"
DEBUG(f"delete_note(id={id}") DEBUG(f"delete_note(id={id}")
session.query(Notes).filter(Notes.id == id).delete() session.query(Notes).filter(Notes.id == id).delete()
session.commit() session.commit()
# Not currently used 1 June 2021
# @staticmethod
# def get_note(session, id):
# return session.query(Notes).filter(Notes.id == id).one()
@classmethod @classmethod
def update_note(cls, session, id, row, text=None): def update_note(cls, session, id, row, text=None):
""" """
@ -99,6 +98,8 @@ class Playdates(Base):
@staticmethod @staticmethod
def add_playdate(session, track): def add_playdate(session, track):
"Record that track was played"
DEBUG(f"add_playdate(track={track})") DEBUG(f"add_playdate(track={track})")
pd = Playdates() pd = Playdates()
pd.lastplayed = datetime.now() pd.lastplayed = datetime.now()
@ -451,6 +452,16 @@ class Tracks(Base):
f"<Track(id={self.id}, title={self.title}, " f"<Track(id={self.id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>" f"artist={self.artist}, path={self.path}>"
) )
# Not currently used 1 June 2021
# @staticmethod
# def get_note(session, id):
# return session.query(Notes).filter(Notes.id == id).one()
@staticmethod
def get_all_paths(session):
"Return a list of paths of all tracks"
return [a[0] for a in session.query(Tracks.path).all()]
@classmethod @classmethod
def get_or_create(cls, session, path): def get_or_create(cls, session, path):
@ -472,53 +483,6 @@ class Tracks(Base):
ERROR(f"Can't find track id {id}") ERROR(f"Can't find track id {id}")
return None return None
@staticmethod
def get_all_paths(session):
"Return a list of paths of all tracks"
return [a[0] for a in session.query(Tracks.path).all()]
@staticmethod
def get_path(session, id):
try:
return session.query(Tracks.path).filter(Tracks.id == id).one()[0]
except NoResultFound:
ERROR(f"Can't find track id {id}")
return None
@staticmethod
def get_track(session, id):
try:
DEBUG(f"Tracks.get_track(track_id={id})")
track = session.query(Tracks).filter(Tracks.id == id).one()
return track
except NoResultFound:
ERROR(f"get_track({id}): not found")
return None
@staticmethod
def search(session, title=None, artist=None, duration=None):
"""
Return any tracks matching passed criteria
"""
DEBUG(
f"Tracks.search({title=}, {artist=}), {duration=})"
)
if not title and not artist and not duration:
return None
q = session.query(Tracks).filter(False)
if title:
q = q.filter(Tracks.title == title)
if artist:
q = q.filter(Tracks.artist == artist)
if duration:
q = q.filter(Tracks.duration == duration)
return q.all()
@staticmethod @staticmethod
def get_track_from_filename(session, filename): def get_track_from_filename(session, filename):
""" """
@ -546,6 +510,29 @@ class Tracks(Base):
return session.query(Tracks).filter(Tracks.path == path).first() return session.query(Tracks).filter(Tracks.path == path).first()
@staticmethod
def get_path(session, track_id):
"Return path of passed track_id, or None"
try:
return session.query(Tracks.path).filter(
Tracks.id == track_id).one()[0]
except NoResultFound:
ERROR(f"Can't find track id {track_id}")
return None
@staticmethod
def get_track(session, track_id):
"Return track or None"
try:
DEBUG(f"Tracks.get_track(track_id={track_id})")
track = session.query(Tracks).filter(Tracks.id == track_id).one()
return track
except NoResultFound:
ERROR(f"get_track({track_id}): not found")
return None
@staticmethod @staticmethod
def remove_path(session, path): def remove_path(session, path):
"Remove track with passed path from database" "Remove track with passed path from database"
@ -558,6 +545,29 @@ class Tracks(Base):
except IntegrityError as exception: except IntegrityError as exception:
ERROR(f"Can't remove track with {path=} ({exception=})") ERROR(f"Can't remove track with {path=} ({exception=})")
@staticmethod
def search(session, title=None, artist=None, duration=None):
"""
Return any tracks matching passed criteria
"""
DEBUG(
f"Tracks.search({title=}, {artist=}), {duration=})"
)
if not title and not artist and not duration:
return None
q = session.query(Tracks).filter(False)
if title:
q = q.filter(Tracks.title == title)
if artist:
q = q.filter(Tracks.artist == artist)
if duration:
q = q.filter(Tracks.duration == duration)
return q.all()
@staticmethod @staticmethod
def search_titles(session, text): def search_titles(session, text):
return ( return (

View File

@ -8,7 +8,6 @@ import urllib.parse
from datetime import datetime, timedelta from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION from log import DEBUG, EXCEPTION
from slugify import slugify
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
@ -18,7 +17,6 @@ from PyQt5.QtWidgets import (
QInputDialog, QInputDialog,
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QMessageBox,
) )
import helpers import helpers
@ -139,10 +137,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionOpenPlaylist.triggered.connect(self.open_playlist) self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionSearch_database.triggered.connect(self.search_database) self.actionSearch_database.triggered.connect(self.search_database)
self.actionSelect_next_track.triggered.connect(self.select_next_track) self.actionSelect_next_track.triggered.connect(self.select_next_row)
self.actionSelect_played_tracks.triggered.connect(self.select_played) self.actionSelect_played_tracks.triggered.connect(self.select_played)
self.actionSelect_previous_track.triggered.connect( self.actionSelect_previous_track.triggered.connect(
self.select_previous_track) self.select_previous_row)
self.actionSelect_unplayed_tracks.triggered.connect( self.actionSelect_unplayed_tracks.triggered.connect(
self.select_unplayed) self.select_unplayed)
self.actionSetNext.triggered.connect(self.set_next_track) self.actionSetNext.triggered.connect(self.set_next_track)
@ -224,6 +222,10 @@ class Window(QMainWindow, Ui_MainWindow):
def end_of_track_actions(self): def end_of_track_actions(self):
"Clean up after track played" "Clean up after track played"
# Set self.playing to False so that tick() doesn't see
# player=None and kick off end-of-track actions
self.playing = False
# Clean up metadata # Clean up metadata
self.previous_track = self.current_track self.previous_track = self.current_track
if self.current_track_playlist_tab: if self.current_track_playlist_tab:
@ -231,7 +233,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.current_track_playlist_tab.clear_current() self.current_track_playlist_tab.clear_current()
self.current_track_playlist_tab = None self.current_track_playlist_tab = None
self.current_track = None self.current_track = None
self.playing = False
# Clean up display # Clean up display
self.label_end_timer.setText("00:00") self.label_end_timer.setText("00:00")
@ -239,6 +240,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
self.update_headers() self.update_headers()
# Enable controls
self.enable_play_next_controls()
def export_playlist_tab(self): def export_playlist_tab(self):
"Export the current playlist to an m3u file" "Export the current playlist to an m3u file"
@ -285,10 +289,6 @@ class Window(QMainWindow, Ui_MainWindow):
if not self.current_track: if not self.current_track:
return return
# Set self.playing to False so that tick() doesn't see
# player=None and kick off end-of-track actions
self.playing = False
self.enable_play_next_controls()
self.previous_track_position = self.music.fade() self.previous_track_position = self.music.fade()
self.end_of_track_actions() self.end_of_track_actions()
@ -415,17 +415,15 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.play(self.current_track.path) self.music.play(self.current_track.path)
# Update metadata # Update metadata
# TODO is this valid if next track is on different playlist? # Get next track for this playlist. May be None if there is
# no automatic next track, and may later be overriden by
# user selecting a different track on this or another
# playlist.
next_track_id = self.current_track_playlist_tab.play_started() next_track_id = self.current_track_playlist_tab.play_started()
if next_track_id is not None: if next_track_id is not None:
self.next_track = Tracks.get_track(session, next_track_id) self.next_track = Tracks.get_track(session, next_track_id)
self.next_track_playlist_tab = self.current_track_playlist_tab self.next_track_playlist_tab = self.current_track_playlist_tab
# Check we can read it
if not self.file_is_readable(self.next_track.path):
self.show_warning(
"Can't read next track",
self.next_track.path)
else: else:
self.next_track = self.next_track_playlist_tab = None self.next_track = self.next_track_playlist_tab = None
@ -466,20 +464,20 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_db = Playlists.open(session, dlg.plid) playlist_db = Playlists.open(session, dlg.plid)
self.load_playlist(session, playlist_db) self.load_playlist(session, playlist_db)
def select_next_track(self): def select_next_row(self):
"Select next or first track in playlist" "Select next or first row in playlist"
self.visible_playlist_tab().select_next_track() self.visible_playlist_tab().select_next_row()
def select_played(self): def select_played(self):
"Select all played tracks in playlist" "Select all played tracks in playlist"
self.visible_playlist_tab().select_played_tracks() self.visible_playlist_tab().select_played_tracks()
def select_previous_track(self): def select_previous_row(self):
"Select previous or first track in playlist" "Select previous or first row in playlist"
self.visible_playlist_tab().select_previous_track() self.visible_playlist_tab().select_previous_row()
def set_next_track(self, next_track_id=None): def set_next_track(self, next_track_id=None):
"Set selected track as next" "Set selected track as next"
@ -501,11 +499,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.visible_playlist_tab().select_unplayed_tracks() self.visible_playlist_tab().select_unplayed_tracks()
def show_warning(self, title, msg):
"Display a warning to user"
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def song_info_search(self): def song_info_search(self):
""" """
Open browser tabs for Wikipedia, searching for Open browser tabs for Wikipedia, searching for
@ -534,12 +527,11 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop()") DEBUG("musicmuster.stop()")
self.stop_playing(fade=False) self.stop_playing(fade=False)
self.enable_play_next_controls()
def stop_playing(self, fade=True): def stop_playing(self, fade=True):
"Stop playing current track" "Stop playing current track"
DEBUG("musicmuster.stop_playing()", True) DEBUG(f"musicmuster.stop_playing({fade=})", True)
if not self.music.playing(): if not self.music.playing():
DEBUG("musicmuster.stop_playing(): not playing", True) DEBUG("musicmuster.stop_playing(): not playing", True)
@ -551,8 +543,8 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop_playing(): fading music", True) DEBUG("musicmuster.stop_playing(): fading music", True)
self.music.fade() self.music.fade()
else: else:
self.music.stop()
DEBUG("musicmuster.stop_playing(): stopping music", True) DEBUG("musicmuster.stop_playing(): stopping music", True)
self.music.stop()
self.end_of_track_actions() self.end_of_track_actions()
@ -620,16 +612,20 @@ class Window(QMainWindow, Ui_MainWindow):
# Time to fade # Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
# Time to silence # If silent in the next 5 seconds, put warning colour on
# time to silence box and enable play controls
if time_to_silence <= 5500: if time_to_silence <= 5500:
self.frame_silent.setStyleSheet( self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}" f"background: {Config.COLOUR_ENDING_TIMER}"
) )
self.enable_play_next_controls() self.enable_play_next_controls()
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500: elif time_to_fade <= 500:
self.frame_silent.setStyleSheet( self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}" f"background: {Config.COLOUR_WARNING_TIMER}"
) )
# Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls
elif time_to_fade <= 5500: elif time_to_fade <= 5500:
self.frame_fade.setStyleSheet( self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}" f"background: {Config.COLOUR_WARNING_TIMER}"

View File

@ -16,7 +16,7 @@ import os
from config import Config from config import Config
from datetime import datetime, timedelta from datetime import datetime, timedelta
from helpers import get_relative_date from helpers import get_relative_date, show_warning
from log import DEBUG, ERROR from log import DEBUG, ERROR
from model import ( from model import (
Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks
@ -155,8 +155,8 @@ class PlaylistTab(QTableWidget):
self.menu.addSeparator() self.menu.addSeparator()
act_delete = self.menu.addAction('Delete') act_delete = self.menu.addAction('Delete')
act_delete.triggered.connect(lambda: self._delete_row(row)) act_delete.triggered.connect(lambda: self._delete_row(row))
act_delete = self.menu.addAction('Info') act_info = self.menu.addAction('Info')
act_delete.triggered.connect(lambda: self._info_row(row)) act_info.triggered.connect(lambda: self._info_row(row))
return super(PlaylistTab, self).eventFilter(source, event) return super(PlaylistTab, self).eventFilter(source, event)
@ -357,7 +357,10 @@ class PlaylistTab(QTableWidget):
# Scroll to put current track in centre # Scroll to put current track in centre
scroll_to = self.item(current_row, self.COL_INDEX) scroll_to = self.item(current_row, self.COL_INDEX)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter)
next_track_id = self._mark_next_track()
# Get next track
next_track_row = self._find_next_track_row()
next_track_id = self._set_next(next_track_row)
self._repaint() self._repaint()
return next_track_id return next_track_id
@ -407,9 +410,9 @@ class PlaylistTab(QTableWidget):
# Called when we change tabs # Called when we change tabs
self._repaint() self._repaint()
def select_next_track(self): def select_next_row(self):
""" """
Select next or first track. Don't select notes. Wrap at last row. Select next or first row. Don't select notes. Wrap at last row.
""" """
selected_rows = [row for row in selected_rows = [row for row in
@ -454,7 +457,7 @@ class PlaylistTab(QTableWidget):
# Reset extended selection # Reset extended selection
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def select_previous_track(self): def select_previous_row(self):
""" """
Select previous or last track. Don't select notes. Wrap at first row. Select previous or last track. Don't select notes. Wrap at first row.
""" """
@ -514,7 +517,8 @@ class PlaylistTab(QTableWidget):
if not self.selectionModel().hasSelection(): if not self.selectionModel().hasSelection():
return return
return self._set_next(self.currentRow()) self._set_next(self.currentRow())
self._repaint()
# ########## Internally called functions ########## # ########## Internally called functions ##########
@ -545,12 +549,10 @@ class PlaylistTab(QTableWidget):
DEBUG(f"playlist._delete_row({row})") DEBUG(f"playlist._delete_row({row})")
if row == self._meta_get_current(): if row == self._meta_get_current():
# TODO show_warning("Silly", "Can't delete playing track")
DEBUG("playlist._delete_row(): Can't delete playing track")
return return
elif row == self._meta_get_next(): elif row == self._meta_get_next():
# TODO show_warning("Safety", "Can't delete next track")
DEBUG("playlist._delete_row(): Can't delete next track")
return return
with Session() as session: with Session() as session:
@ -648,13 +650,13 @@ class PlaylistTab(QTableWidget):
and pos.y() >= rect.center().y() # noqa W503 and pos.y() >= rect.center().y() # noqa W503
) )
def _mark_next_track(self): def _find_next_track_row(self):
""" """
Find next track to play. Find next track to play.
If not found, return None. If not found, return None.
If found, mark row with metadata and return track_id. If found, return row number.
""" """
found_next_track = False found_next_track = False
@ -667,16 +669,14 @@ class PlaylistTab(QTableWidget):
for row in range(start, self.rowCount()): for row in range(start, self.rowCount()):
if row in notes_rows: if row in notes_rows:
continue continue
self._meta_set_next(row)
found_next_track = True found_next_track = True
break break
if not found_next_track: if found_next_track:
return row
else:
return None return None
track_id = self._get_row_id(row)
return track_id
def _meta_clear(self, row): def _meta_clear(self, row):
"Clear metadata for row" "Clear metadata for row"
@ -813,11 +813,12 @@ class PlaylistTab(QTableWidget):
track_id = self._get_row_id(row) track_id = self._get_row_id(row)
if track_id: if track_id:
if self._track_path_is_readable(track_id): if self._track_path_is_readable(track_id):
self._meta_set_next(self.currentRow()) self._meta_set_next(row)
self.master_process.set_next_track(track_id) self.master_process.set_next_track(track_id)
else: else:
self._meta_set_unreadable(self.currentRow()) self._meta_set_unreadable(row)
self._repaint() track_id = None
return track_id
def _repaint(self, clear_selection=True): def _repaint(self, clear_selection=True):
"Set row colours, fonts, etc" "Set row colours, fonts, etc"

View File

@ -31,12 +31,15 @@ def fade_point(audio_segment, fade_threshold=-12, chunk_size=10):
print(f"Fade last {int(segment_length - trim_ms)/1000} seconds") print(f"Fade last {int(segment_length - trim_ms)/1000} seconds")
print("Shout:") # print("Shout:")
segment = AudioSegment.from_mp3("../archive/shout.mp3") # segment = AudioSegment.from_mp3("../archive/shout.mp3")
fade_point(segment) # fade_point(segment)
print("Champagne:") # print("Champagne:")
segment = AudioSegment.from_mp3("../archive/champ.mp3") # segment = AudioSegment.from_mp3("../archive/champ.mp3")
fade_point(segment) # fade_point(segment)
# print("Be good:")
# segment = AudioSegment.from_mp3("../archive/wibg.mp3")
# fade_point(segment)
print("Be good:") print("Be good:")
segment = AudioSegment.from_mp3("../archive/wibg.mp3") segment = AudioSegment.from_file("/tmp/bia.flac", "flac")
fade_point(segment) fade_point(segment)