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):
@ -36,6 +37,12 @@ def get_relative_date(past_date, reference_date=None):
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):
if not ms:
return "-"

View File

@ -2,7 +2,7 @@
import sqlalchemy
from datetime import datetime, time
from datetime import datetime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
Boolean,
@ -53,6 +53,8 @@ class Notes(Base):
@staticmethod
def add_note(session, playlist_id, row, text):
"Add note"
DEBUG(f"add_note(playlist_id={playlist_id}, row={row}, text={text})")
note = Notes()
note.playlist_id = playlist_id
@ -64,16 +66,13 @@ class Notes(Base):
@staticmethod
def delete_note(session, id):
"Delete note"
DEBUG(f"delete_note(id={id}")
session.query(Notes).filter(Notes.id == id).delete()
session.commit()
# Not currently used 1 June 2021
# @staticmethod
# def get_note(session, id):
# return session.query(Notes).filter(Notes.id == id).one()
@classmethod
def update_note(cls, session, id, row, text=None):
"""
@ -99,6 +98,8 @@ class Playdates(Base):
@staticmethod
def add_playdate(session, track):
"Record that track was played"
DEBUG(f"add_playdate(track={track})")
pd = Playdates()
pd.lastplayed = datetime.now()
@ -451,6 +452,16 @@ class Tracks(Base):
f"<Track(id={self.id}, title={self.title}, "
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
def get_or_create(cls, session, path):
@ -472,53 +483,6 @@ class Tracks(Base):
ERROR(f"Can't find track id {id}")
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
def get_track_from_filename(session, filename):
"""
@ -546,6 +510,29 @@ class Tracks(Base):
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
def remove_path(session, path):
"Remove track with passed path from database"
@ -558,6 +545,29 @@ class Tracks(Base):
except IntegrityError as 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
def search_titles(session, text):
return (

View File

@ -8,7 +8,6 @@ import urllib.parse
from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION
from slugify import slugify
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import (
@ -18,7 +17,6 @@ from PyQt5.QtWidgets import (
QInputDialog,
QListWidgetItem,
QMainWindow,
QMessageBox,
)
import helpers
@ -139,10 +137,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next)
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_previous_track.triggered.connect(
self.select_previous_track)
self.select_previous_row)
self.actionSelect_unplayed_tracks.triggered.connect(
self.select_unplayed)
self.actionSetNext.triggered.connect(self.set_next_track)
@ -224,6 +222,10 @@ class Window(QMainWindow, Ui_MainWindow):
def end_of_track_actions(self):
"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
self.previous_track = self.current_track
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 = None
self.current_track = None
self.playing = False
# Clean up display
self.label_end_timer.setText("00:00")
@ -239,6 +240,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_fade.setStyleSheet("")
self.update_headers()
# Enable controls
self.enable_play_next_controls()
def export_playlist_tab(self):
"Export the current playlist to an m3u file"
@ -285,10 +289,6 @@ class Window(QMainWindow, Ui_MainWindow):
if not self.current_track:
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.end_of_track_actions()
@ -415,17 +415,15 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.play(self.current_track.path)
# 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()
if next_track_id is not None:
self.next_track = Tracks.get_track(session, next_track_id)
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:
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)
self.load_playlist(session, playlist_db)
def select_next_track(self):
"Select next or first track in playlist"
def select_next_row(self):
"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):
"Select all played tracks in playlist"
self.visible_playlist_tab().select_played_tracks()
def select_previous_track(self):
"Select previous or first track in playlist"
def select_previous_row(self):
"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):
"Set selected track as next"
@ -501,11 +499,6 @@ class Window(QMainWindow, Ui_MainWindow):
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):
"""
Open browser tabs for Wikipedia, searching for
@ -534,12 +527,11 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop()")
self.stop_playing(fade=False)
self.enable_play_next_controls()
def stop_playing(self, fade=True):
"Stop playing current track"
DEBUG("musicmuster.stop_playing()", True)
DEBUG(f"musicmuster.stop_playing({fade=})", True)
if not self.music.playing():
DEBUG("musicmuster.stop_playing(): not playing", True)
@ -551,8 +543,8 @@ class Window(QMainWindow, Ui_MainWindow):
DEBUG("musicmuster.stop_playing(): fading music", True)
self.music.fade()
else:
self.music.stop()
DEBUG("musicmuster.stop_playing(): stopping music", True)
self.music.stop()
self.end_of_track_actions()
@ -620,16 +612,20 @@ class Window(QMainWindow, Ui_MainWindow):
# 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:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
)
self.enable_play_next_controls()
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500:
self.frame_silent.setStyleSheet(
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:
self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"

View File

@ -16,7 +16,7 @@ import os
from config import Config
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 model import (
Notes, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks
@ -155,8 +155,8 @@ class PlaylistTab(QTableWidget):
self.menu.addSeparator()
act_delete = self.menu.addAction('Delete')
act_delete.triggered.connect(lambda: self._delete_row(row))
act_delete = self.menu.addAction('Info')
act_delete.triggered.connect(lambda: self._info_row(row))
act_info = self.menu.addAction('Info')
act_info.triggered.connect(lambda: self._info_row(row))
return super(PlaylistTab, self).eventFilter(source, event)
@ -357,7 +357,10 @@ class PlaylistTab(QTableWidget):
# Scroll to put current track in centre
scroll_to = self.item(current_row, self.COL_INDEX)
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()
return next_track_id
@ -407,9 +410,9 @@ class PlaylistTab(QTableWidget):
# Called when we change tabs
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
@ -454,7 +457,7 @@ class PlaylistTab(QTableWidget):
# Reset extended selection
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.
"""
@ -514,7 +517,8 @@ class PlaylistTab(QTableWidget):
if not self.selectionModel().hasSelection():
return
return self._set_next(self.currentRow())
self._set_next(self.currentRow())
self._repaint()
# ########## Internally called functions ##########
@ -545,12 +549,10 @@ class PlaylistTab(QTableWidget):
DEBUG(f"playlist._delete_row({row})")
if row == self._meta_get_current():
# TODO
DEBUG("playlist._delete_row(): Can't delete playing track")
show_warning("Silly", "Can't delete playing track")
return
elif row == self._meta_get_next():
# TODO
DEBUG("playlist._delete_row(): Can't delete next track")
show_warning("Safety", "Can't delete next track")
return
with Session() as session:
@ -648,13 +650,13 @@ class PlaylistTab(QTableWidget):
and pos.y() >= rect.center().y() # noqa W503
)
def _mark_next_track(self):
def _find_next_track_row(self):
"""
Find next track to play.
If not found, return None.
If found, mark row with metadata and return track_id.
If found, return row number.
"""
found_next_track = False
@ -667,16 +669,14 @@ class PlaylistTab(QTableWidget):
for row in range(start, self.rowCount()):
if row in notes_rows:
continue
self._meta_set_next(row)
found_next_track = True
break
if not found_next_track:
if found_next_track:
return row
else:
return None
track_id = self._get_row_id(row)
return track_id
def _meta_clear(self, row):
"Clear metadata for row"
@ -813,11 +813,12 @@ class PlaylistTab(QTableWidget):
track_id = self._get_row_id(row)
if 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)
else:
self._meta_set_unreadable(self.currentRow())
self._repaint()
self._meta_set_unreadable(row)
track_id = None
return track_id
def _repaint(self, clear_selection=True):
"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("Shout:")
segment = AudioSegment.from_mp3("../archive/shout.mp3")
fade_point(segment)
print("Champagne:")
segment = AudioSegment.from_mp3("../archive/champ.mp3")
fade_point(segment)
# print("Shout:")
# segment = AudioSegment.from_mp3("../archive/shout.mp3")
# fade_point(segment)
# print("Champagne:")
# segment = AudioSegment.from_mp3("../archive/champ.mp3")
# fade_point(segment)
# print("Be good:")
# segment = AudioSegment.from_mp3("../archive/wibg.mp3")
# fade_point(segment)
print("Be good:")
segment = AudioSegment.from_mp3("../archive/wibg.mp3")
segment = AudioSegment.from_file("/tmp/bia.flac", "flac")
fade_point(segment)