Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc6fbe0d0 | ||
|
|
e689c9afeb | ||
|
|
a0fa7e455e | ||
|
|
bcb8a95969 | ||
|
|
8674e6d5b3 | ||
|
|
705f3ea2f2 | ||
|
|
405efee732 | ||
|
|
2db407edc5 | ||
|
|
ab8da0a312 | ||
|
|
48d26d80df | ||
|
|
da751ee530 | ||
|
|
282e4476a9 | ||
|
|
ecd46b8a0a |
@ -53,8 +53,9 @@ class Config(object):
|
||||
FADE_CURVE_BACKGROUND = "lightyellow"
|
||||
FADE_CURVE_FOREGROUND = "blue"
|
||||
FADE_CURVE_MS_BEFORE_FADE = 5000
|
||||
FADE_STEPS = 20
|
||||
FADE_TIME = 3000
|
||||
FADEOUT_DB = -10
|
||||
FADEOUT_SECONDS = 5
|
||||
FADEOUT_STEPS_PER_SECOND = 5
|
||||
HIDE_AFTER_PLAYING_OFFSET = 5000
|
||||
INFO_TAB_TITLE_LENGTH = 15
|
||||
LAST_PLAYED_TODAY_STRING = "Today"
|
||||
|
||||
45
app/music.py
45
app/music.py
@ -19,11 +19,12 @@ lock = threading.Lock()
|
||||
|
||||
|
||||
class FadeTrack(QRunnable):
|
||||
def __init__(self, player: vlc.MediaPlayer) -> None:
|
||||
def __init__(self, player: vlc.MediaPlayer, fade_seconds) -> None:
|
||||
super().__init__()
|
||||
self.player = player
|
||||
self.fade_seconds = fade_seconds
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Implementation of fading the player
|
||||
"""
|
||||
@ -31,24 +32,18 @@ class FadeTrack(QRunnable):
|
||||
if not self.player:
|
||||
return
|
||||
|
||||
fade_time = Config.FADE_TIME / 1000
|
||||
steps = Config.FADE_STEPS
|
||||
sleep_time = fade_time / steps
|
||||
original_volume = self.player.audio_get_volume()
|
||||
# Reduce volume logarithmically
|
||||
total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND
|
||||
db_reduction_per_step = Config.FADEOUT_DB / total_steps
|
||||
reduction_factor_per_step = pow(10, (db_reduction_per_step / 20))
|
||||
|
||||
# We reduce volume by one mesure first, then by two measures,
|
||||
# then three, and so on.
|
||||
# The sum of the arithmetic sequence 1, 2, 3, ..n is
|
||||
# (n**2 + n) / 2
|
||||
total_measures_count = (steps**2 + steps) / 2
|
||||
volume = self.player.audio_get_volume()
|
||||
|
||||
measures_to_reduce_by = 0
|
||||
|
||||
for i in range(1, steps + 1):
|
||||
measures_to_reduce_by += i
|
||||
volume_factor = 1 - (measures_to_reduce_by / total_measures_count)
|
||||
self.player.audio_set_volume(int(original_volume * volume_factor))
|
||||
sleep(sleep_time)
|
||||
for i in range(1, total_steps + 1):
|
||||
self.player.audio_set_volume(
|
||||
int(volume * pow(reduction_factor_per_step, i))
|
||||
)
|
||||
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
|
||||
|
||||
self.player.stop()
|
||||
log.debug(f"Releasing player {self.player=}")
|
||||
@ -65,7 +60,7 @@ class Music:
|
||||
self.player = None
|
||||
self.max_volume = Config.VOLUME_VLC_DEFAULT
|
||||
|
||||
def fade(self) -> None:
|
||||
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
|
||||
"""
|
||||
Fade the currently playing track.
|
||||
|
||||
@ -86,7 +81,7 @@ class Music:
|
||||
self.player = None
|
||||
|
||||
pool = QThreadPool.globalInstance()
|
||||
fader = FadeTrack(p)
|
||||
fader = FadeTrack(p, fade_seconds=fade_seconds)
|
||||
pool.start(fader)
|
||||
|
||||
def get_position(self) -> Optional[float]:
|
||||
@ -96,7 +91,7 @@ class Music:
|
||||
return None
|
||||
return self.player.get_position()
|
||||
|
||||
def play(self, path: str, position: Optional[float] = None) -> Optional[int]:
|
||||
def play(self, path: str, position: Optional[float] = None) -> None:
|
||||
"""
|
||||
Start playing the track at path.
|
||||
|
||||
@ -107,19 +102,15 @@ class Music:
|
||||
log.error(f"play({path}): path not readable")
|
||||
return None
|
||||
|
||||
status = -1
|
||||
|
||||
media = self.VLC.media_new_path(path)
|
||||
self.player = media.player_new_from_media()
|
||||
if self.player:
|
||||
status = self.player.play()
|
||||
_ = self.player.play()
|
||||
self.set_volume(self.max_volume)
|
||||
if position:
|
||||
self.player.set_position(position)
|
||||
|
||||
return status
|
||||
|
||||
def set_volume(self, volume=None, set_default=True):
|
||||
def set_volume(self, volume=None, set_default=True) -> None:
|
||||
"""Set maximum volume used for player"""
|
||||
|
||||
if not self.player:
|
||||
|
||||
@ -223,7 +223,7 @@ class ImportTrack(QObject):
|
||||
print(e)
|
||||
return
|
||||
helpers.normalise_track(track.path)
|
||||
self.playlist.insert_track(session, track, target_row)
|
||||
self.playlist.insert_track(session=session, track=track, target_row=target_row)
|
||||
# Insert next row under this one
|
||||
target_row += 1
|
||||
# We're importing potentially multiple tracks in a loop.
|
||||
@ -1318,6 +1318,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if self.btnDrop3db.isChecked():
|
||||
self.btnDrop3db.setChecked(False)
|
||||
|
||||
# Show closing volume graph
|
||||
self.current_track.fade_graph.plot()
|
||||
|
||||
# Play (new) current track
|
||||
self.current_track.start()
|
||||
self.music.play(self.current_track.path, position)
|
||||
@ -1334,9 +1337,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
break
|
||||
sleep(0.1)
|
||||
|
||||
# Show closing volume graph
|
||||
self.current_track.fade_graph.plot()
|
||||
|
||||
# Tell database to record it as played
|
||||
Playdates(session, self.current_track.track_id)
|
||||
|
||||
|
||||
123
app/playlists.py
123
app/playlists.py
@ -249,18 +249,30 @@ class PlaylistTab(QTableWidget):
|
||||
else:
|
||||
rowMapping[row + len(rows)] = targetRow + idx
|
||||
colCount = self.columnCount()
|
||||
for srcRow, tgtRow in sorted(rowMapping.items()):
|
||||
if self._get_row_track_id(srcRow):
|
||||
# This is a track row
|
||||
with Session() as session:
|
||||
for srcRow, tgtRow in sorted(rowMapping.items()):
|
||||
# Messy: will be fixed with model/view implementation.
|
||||
# If we just move the row, the displayed text will be
|
||||
# used. That is incorrect for timing starts, ends and
|
||||
# subtotals, so take text from database and use that.
|
||||
is_header_row = self._get_row_track_id(srcRow) == 0
|
||||
if is_header_row:
|
||||
# This is a header row so save original text from db
|
||||
source_plr = self._get_row_plr(session, srcRow)
|
||||
if not source_plr:
|
||||
print("Can't get source_plr in playlists:dropEvent()")
|
||||
return
|
||||
note_text = source_plr.note
|
||||
# Copy to new locations
|
||||
for col in range(0, colCount):
|
||||
self.setItem(tgtRow, col, self.takeItem(srcRow, col))
|
||||
else:
|
||||
self.setItem(
|
||||
tgtRow,
|
||||
HEADER_NOTES_COLUMN,
|
||||
self.takeItem(srcRow, HEADER_NOTES_COLUMN),
|
||||
)
|
||||
self.setSpan(tgtRow, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
|
||||
|
||||
# Fixup header text
|
||||
if is_header_row:
|
||||
target_item = self.item(tgtRow, HEADER_NOTES_COLUMN)
|
||||
if target_item:
|
||||
target_item.setText(note_text)
|
||||
self.setSpan(tgtRow, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
|
||||
for row in reversed(sorted(rowMapping.keys())):
|
||||
self.removeRow(row)
|
||||
self.resizeRowsToContents()
|
||||
@ -640,8 +652,8 @@ class PlaylistTab(QTableWidget):
|
||||
self,
|
||||
session: scoped_session,
|
||||
track: Tracks,
|
||||
note: str = "",
|
||||
repaint: bool = True,
|
||||
note: Optional[str] = "",
|
||||
repaint: Optional[bool] = True,
|
||||
target_row: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
@ -1449,20 +1461,32 @@ class PlaylistTab(QTableWidget):
|
||||
return userdata_item.data(role)
|
||||
|
||||
def _get_section_timing_string(
|
||||
self, total_time: int, unplayed_time: int, no_end: bool = False
|
||||
self,
|
||||
unplayed_time: int,
|
||||
end_time: Optional[datetime] = None,
|
||||
no_end: bool = False,
|
||||
) -> str:
|
||||
"""Return string describing section duration"""
|
||||
"""
|
||||
Return string describing section duration. If end_time specified, also
|
||||
return section end time calculated as end_time + unplayed duration.
|
||||
"""
|
||||
|
||||
total_duration = ms_to_mmss(total_time)
|
||||
if unplayed_time:
|
||||
unplayed_duration = ms_to_mmss(unplayed_time)
|
||||
unplayed_duration = ms_to_mmss(unplayed_time)
|
||||
if end_time:
|
||||
section_end_time = end_time + timedelta(milliseconds=unplayed_time)
|
||||
end_str = (
|
||||
"[End time for all unplayed tracks in section: "
|
||||
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT)
|
||||
+ "]"
|
||||
)
|
||||
else:
|
||||
unplayed_duration = "[No unplayed tracks]"
|
||||
end_str = ""
|
||||
|
||||
caveat = ""
|
||||
if no_end:
|
||||
caveat = " (to end of playlist)"
|
||||
|
||||
return f" {unplayed_duration} ({total_duration}){caveat}"
|
||||
return f" {unplayed_duration} {caveat} {end_str}"
|
||||
|
||||
def _get_selected_row(self) -> Optional[int]:
|
||||
"""
|
||||
@ -2204,7 +2228,7 @@ class PlaylistTab(QTableWidget):
|
||||
if not start_gap:
|
||||
start_gap = 0
|
||||
start_gap_item = self._set_item_text(row_number, START_GAP, str(start_gap))
|
||||
if start_gap >= 500:
|
||||
if start_gap >= 300:
|
||||
brush = QBrush(QColor(Config.COLOUR_LONG_START))
|
||||
else:
|
||||
brush = QBrush()
|
||||
@ -2386,26 +2410,24 @@ class PlaylistTab(QTableWidget):
|
||||
self.save_playlist(session)
|
||||
self._update_start_end_times(session)
|
||||
|
||||
def _track_time_between_rows(
|
||||
def _unplayed_track_time_between_rows(
|
||||
self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows
|
||||
) -> Tuple[int, int]:
|
||||
) -> int:
|
||||
"""
|
||||
Returns the (total duration of all tracks in rows between
|
||||
from_row and to_row inclusive, total unplayed time in those rows)
|
||||
Returns the total unplayed duration of all tracks in rows between
|
||||
from_row and to_row inclusive
|
||||
"""
|
||||
|
||||
plr_tracks = PlaylistRows.get_rows_with_tracks(
|
||||
session, self.playlist_id, from_plr.plr_rownum, to_plr.plr_rownum
|
||||
)
|
||||
|
||||
total_time = 0
|
||||
total_time = sum([a.track.duration for a in plr_tracks if a.track.duration])
|
||||
unplayed_time = 0
|
||||
unplayed_time = sum(
|
||||
[a.track.duration for a in plr_tracks if a.track.duration and not a.played]
|
||||
)
|
||||
|
||||
return (total_time, unplayed_time)
|
||||
return unplayed_time
|
||||
|
||||
def _update_row_track_info(
|
||||
self, session: scoped_session, row: int, track: Tracks
|
||||
@ -2439,6 +2461,21 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
section_start_rows: List[PlaylistRows] = []
|
||||
subtotal_from: Optional[PlaylistRows] = None
|
||||
active_row: Optional[int] = None
|
||||
active_endtime: Optional[datetime] = None
|
||||
current_row_prlid = self.musicmuster.current_track.plr_id
|
||||
if current_row_prlid:
|
||||
current_row = self._plrid_to_row_number(current_row_prlid)
|
||||
if current_row:
|
||||
active_row = current_row
|
||||
active_end = self.musicmuster.current_track.end_time
|
||||
else:
|
||||
previous_row_plrid = self.musicmuster.previous_track.plr_id
|
||||
if previous_row_plrid:
|
||||
previous_row = self._plrid_to_row_number(previous_row_plrid)
|
||||
if previous_row:
|
||||
active_row = previous_row
|
||||
active_end = self.musicmuster.previous_track.end_time
|
||||
|
||||
header_rows = [
|
||||
self._get_row_plr_id(row_number)
|
||||
@ -2457,12 +2494,19 @@ class PlaylistTab(QTableWidget):
|
||||
try:
|
||||
from_plr = section_start_rows.pop()
|
||||
to_plr = plr
|
||||
total_time, unplayed_time = self._track_time_between_rows(
|
||||
unplayed_time = self._unplayed_track_time_between_rows(
|
||||
session, from_plr, to_plr
|
||||
)
|
||||
time_str = self._get_section_timing_string(
|
||||
total_time, unplayed_time
|
||||
)
|
||||
if (
|
||||
active_row
|
||||
and active_row >= from_plr.plr_rownum
|
||||
and active_row <= to_plr.plr_rownum
|
||||
):
|
||||
time_str = self._get_section_timing_string(
|
||||
unplayed_time, active_end
|
||||
)
|
||||
else:
|
||||
time_str = self._get_section_timing_string(unplayed_time, None)
|
||||
self._set_row_header_text(
|
||||
session, from_plr.plr_rownum, from_plr.note + time_str
|
||||
)
|
||||
@ -2491,10 +2535,19 @@ class PlaylistTab(QTableWidget):
|
||||
return
|
||||
from_plr = subtotal_from
|
||||
to_plr = plr
|
||||
total_time, unplayed_time = self._track_time_between_rows(
|
||||
unplayed_time = self._unplayed_track_time_between_rows(
|
||||
session, subtotal_from, to_plr
|
||||
)
|
||||
time_str = self._get_section_timing_string(total_time, unplayed_time)
|
||||
if (
|
||||
active_row
|
||||
and active_row >= from_plr.plr_rownum
|
||||
and active_row <= to_plr.plr_rownum
|
||||
):
|
||||
time_str = self._get_section_timing_string(
|
||||
unplayed_time, active_end
|
||||
)
|
||||
else:
|
||||
time_str = self._get_section_timing_string(unplayed_time, None)
|
||||
|
||||
if to_plr.note.strip() == "=":
|
||||
leader_text = "Subtotal: "
|
||||
@ -2510,11 +2563,11 @@ class PlaylistTab(QTableWidget):
|
||||
if possible_plr:
|
||||
to_plr = possible_plr
|
||||
for from_plr in section_start_rows:
|
||||
total_time, unplayed_time = self._track_time_between_rows(
|
||||
unplayed_time = self._unplayed_track_time_between_rows(
|
||||
session, from_plr, to_plr
|
||||
)
|
||||
time_str = self._get_section_timing_string(
|
||||
total_time, unplayed_time, no_end=True
|
||||
unplayed_time, no_end=True
|
||||
)
|
||||
self._set_row_header_text(
|
||||
session, from_plr.plr_rownum, from_plr.note + time_str
|
||||
|
||||
@ -75,7 +75,13 @@ def main():
|
||||
continue
|
||||
new_tags = get_tags(new_path)
|
||||
new_title = new_tags["title"]
|
||||
if not new_title:
|
||||
print(f"{new_fname} does not have a title tag")
|
||||
sys.exit(1)
|
||||
new_artist = new_tags["artist"]
|
||||
if not new_artist:
|
||||
print(f"{new_fname} does not have an artist tag")
|
||||
sys.exit(1)
|
||||
bitrate = new_tags["bitrate"]
|
||||
|
||||
# If same filename exists in parent direcory, check tags
|
||||
@ -255,7 +261,7 @@ def process_track(src, dst, title, artist, bitrate):
|
||||
shutil.move(src, new_path)
|
||||
|
||||
# Update track metadata
|
||||
set_track_metadata(session, track)
|
||||
set_track_metadata(track)
|
||||
|
||||
|
||||
main()
|
||||
|
||||
939
poetry.lock
generated
939
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user