media player controls working

This commit is contained in:
billypom on debian 2025-04-19 13:43:19 -04:00
parent c3945c51b8
commit 466b38ce2e
5 changed files with 87 additions and 48 deletions

View File

@ -75,6 +75,7 @@ player = new QMediaPlayer;
player->setMedia(QUrl("gst-pipeline: videotestsrc ! autovideosink")); player->setMedia(QUrl("gst-pipeline: videotestsrc ! autovideosink"));
player->play(); player->play();
``` ```
QMultimedia.EncodingMode / Encoding quality...
##### misc ##### misc
- database playlist autoexporting - database playlist autoexporting
- .wav, .ogg, .flac convertor - .wav, .ogg, .flac convertor

View File

@ -1,19 +1,18 @@
from PyQt5.QtCore import QObject, QUrl from PyQt5.QtCore import QObject, QUrl, pyqtSignal
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
import sys import sys
class MediaPlayer(QMediaPlayer): class MediaPlayer(QMediaPlayer):
playlistNextSignal = pyqtSignal()
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# Connect mediaStatusChanged signal to our custom function # Connect mediaStatusChanged signal to our custom function
self.mediaStatusChanged.connect(self.on_media_status_changed) self.mediaStatusChanged.connect(self.on_media_status_changed)
# def play(self, file_path): def play(self):
# media_content = QMediaContent(QUrl.fromLocalFile(file_path)) super().play()
# self.player.setMedia(media_content)
# self.player.play()
def on_media_status_changed(self, status): def on_media_status_changed(self, status):
if status == QMediaPlayer.MediaStatus.EndOfMedia: if status == QMediaPlayer.MediaStatus.EndOfMedia:
@ -21,6 +20,6 @@ class MediaPlayer(QMediaPlayer):
self.on_song_ended() self.on_song_ended()
def on_song_ended(self): def on_song_ended(self):
# Your custom logic when the song ends self.playlistNextSignal.emit()
print("Custom function executed after song ended!") print("Custom function executed after song ended!")

View File

@ -67,6 +67,7 @@ from configparser import ConfigParser
class MusicTable(QTableView): class MusicTable(QTableView):
playlistStatsSignal = pyqtSignal(str) playlistStatsSignal = pyqtSignal(str)
loadMusicTableSignal = pyqtSignal() loadMusicTableSignal = pyqtSignal()
sortSignal = pyqtSignal()
playPauseSignal = pyqtSignal() playPauseSignal = pyqtSignal()
playSignal = pyqtSignal(str) playSignal = pyqtSignal(str)
enterKey = pyqtSignal() enterKey = pyqtSignal()
@ -352,12 +353,14 @@ class MusicTable(QTableView):
self.set_selected_song_qmodel_index(selected_qmodel_index) self.set_selected_song_qmodel_index(selected_qmodel_index)
self.set_current_song_qmodel_index(current_qmodel_index) self.set_current_song_qmodel_index(current_qmodel_index)
self.jump_to_selected_song() self.jump_to_selected_song()
self.sortSignal.emit()
def on_cell_clicked(self, index): def on_cell_clicked(self, index):
""" """
When a cell is clicked, do some stuff :) When a cell is clicked, do some stuff :)
- this func also runs when double click happens, fyi - this func also runs when double click happens, fyi
""" """
print(index.row(), index.column())
self.set_selected_song_filepath() self.set_selected_song_filepath()
self.set_selected_song_qmodel_index() self.set_selected_song_qmodel_index()
self.viewport().update() # type: ignore self.viewport().update() # type: ignore
@ -457,6 +460,7 @@ class MusicTable(QTableView):
Sets the current song filepath Sets the current song filepath
Emits a signal that the current song should start playback Emits a signal that the current song should start playback
""" """
self.set_current_song_qmodel_index()
self.set_current_song_filepath() self.set_current_song_filepath()
self.playSignal.emit(self.current_song_filepath) self.playSignal.emit(self.current_song_filepath)
@ -653,6 +657,7 @@ class MusicTable(QTableView):
def toggle_play_pause(self): def toggle_play_pause(self):
"""Toggles the currently playing song by emitting a Signal""" """Toggles the currently playing song by emitting a Signal"""
if not self.current_song_filepath: if not self.current_song_filepath:
self.set_current_song_qmodel_index()
self.set_current_song_filepath() self.set_current_song_filepath()
self.playPauseSignal.emit() self.playPauseSignal.emit()
@ -847,10 +852,6 @@ class MusicTable(QTableView):
"""Returns the selected song's ID3 tags""" """Returns the selected song's ID3 tags"""
return id3_remap(get_tags(self.selected_song_filepath)[0]) return id3_remap(get_tags(self.selected_song_filepath)[0])
def get_current_song_album_art(self) -> bytes:
"""Returns the APIC data (album art lol) for the currently playing song"""
return get_album_art(self.current_song_filepath)
def set_selected_song_filepath(self) -> None: def set_selected_song_filepath(self) -> None:
"""Sets the filepath of the currently selected song""" """Sets the filepath of the currently selected song"""
try: try:
@ -864,13 +865,11 @@ class MusicTable(QTableView):
filepath = db.query('SELECT filepath FROM song WHERE id = ?', (id,))[0][0] filepath = db.query('SELECT filepath FROM song WHERE id = ?', (id,))[0][0]
self.selected_song_filepath = filepath self.selected_song_filepath = filepath
def set_current_song_filepath(self) -> None: def set_current_song_filepath(self, filepath=None) -> None:
""" """
- Sets the current song filepath to the value in column 'path' - Sets the current song filepath to the value in column 'path'
from the current selected row index from the current selected row index
- Store the QModelIndex for navigation
""" """
self.set_current_song_qmodel_index()
# update the filepath # update the filepath
self.current_song_filepath: str = ( self.current_song_filepath: str = (
self.current_song_qmodel_index.siblingAtColumn( self.current_song_qmodel_index.siblingAtColumn(
@ -879,18 +878,28 @@ class MusicTable(QTableView):
) )
def set_current_song_qmodel_index(self, index=None): def set_current_song_qmodel_index(self, index=None):
"""
Takes in the proxy model index for current song - QModelIndex
converts to model2 index
stores it
"""
if index is None: if index is None:
index = self.currentIndex()
# map proxy (sortable) model to the original model (used for interactions) # map proxy (sortable) model to the original model (used for interactions)
index = self.proxymodel.mapToSource(self.currentIndex()) model_index: QModelIndex = self.proxymodel.mapToSource(index)
# set the proxy model index self.current_song_qmodel_index: QModelIndex = model_index
self.current_song_qmodel_index: QModelIndex = index
def set_selected_song_qmodel_index(self, index=None): def set_selected_song_qmodel_index(self, index=None):
"""
Takes in the proxy model index for current song - QModelIndex
converts to model2 index
stores it
"""
if index is None: if index is None:
index = self.currentIndex()
# map proxy (sortable) model to the original model (used for interactions) # map proxy (sortable) model to the original model (used for interactions)
index = self.proxymodel.mapToSource(self.currentIndex()) model_index: QModelIndex = self.proxymodel.mapToSource(index)
# set the proxy model index self.selected_song_qmodel_index: QModelIndex = model_index
self.selected_song_qmodel_index: QModelIndex = index
def load_qapp(self, qapp) -> None: def load_qapp(self, qapp) -> None:
"""Necessary for using members and methods of main application window""" """Necessary for using members and methods of main application window"""

72
main.py
View File

@ -26,8 +26,10 @@ from PyQt5.QtWidgets import (
QPushButton, QPushButton,
QStatusBar, QStatusBar,
QStyle, QStyle,
QTableView,
) )
from PyQt5.QtCore import ( from PyQt5.QtCore import (
QModelIndex,
QSize, QSize,
QThread, QThread,
QUrl, QUrl,
@ -39,7 +41,7 @@ from PyQt5.QtCore import (
QThreadPool, QThreadPool,
QRunnable, QRunnable,
) )
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist, QMultimedia
from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent
from utils import ( from utils import (
delete_album_art, delete_album_art,
@ -48,9 +50,12 @@ from utils import (
initialize_db, initialize_db,
add_files_to_database, add_files_to_database,
set_album_art, set_album_art,
id3_remap
) )
from components import ( from components import (
HeaderTags,
MediaPlayer, MediaPlayer,
MusicTable,
PreferencesWindow, PreferencesWindow,
AudioVisualizer, AudioVisualizer,
CreatePlaylistWindow, CreatePlaylistWindow,
@ -168,10 +173,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.current_song_album_art: bytes | None = None self.current_song_album_art: bytes | None = None
# widget bits # widget bits
self.tableView: MusicTable
self.album_art_scene: QGraphicsScene = QGraphicsScene() self.album_art_scene: QGraphicsScene = QGraphicsScene()
# self.player: QMediaPlayer = QMediaPlayer() # Audio player object # self.player: QMediaPlayer = QMediaPlayer() # Audio player object
self.player: QMediaPlayer = MediaPlayer() self.player: QMediaPlayer = MediaPlayer()
self.playlist: QMediaPlaylist = QMediaPlaylist()
# set index on choose song # set index on choose song
# index is the model2's row number? i guess? # index is the model2's row number? i guess?
self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data self.probe: QAudioProbe = QAudioProbe() # Gets audio buffer data
@ -200,6 +205,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# sharing functions with other classes and that # sharing functions with other classes and that
self.tableView.load_qapp(self) self.tableView.load_qapp(self)
self.albumGraphicsView.load_qapp(self) self.albumGraphicsView.load_qapp(self)
self.headers = HeaderTags()
# Settings init # Settings init
self.current_volume: int = int(self.config["settings"]["volume"]) self.current_volume: int = int(self.config["settings"]["volume"])
@ -224,7 +230,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
) )
# self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1)) # self.speedSlider.doubleClicked.connect(lambda: self.on_speed_changed(1))
self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause self.playButton.clicked.connect(self.on_play_clicked) # Click to play/pause
self.previousButton.clicked.connect(self.on_previous_clicked) self.previousButton.clicked.connect(self.on_prev_clicked)
self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song self.nextButton.clicked.connect(self.on_next_clicked) # Click to next song
# FILE MENU # FILE MENU
@ -257,8 +263,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.tableView.playlistStatsSignal.connect( self.tableView.playlistStatsSignal.connect(
self.set_permanent_status_bar_message self.set_permanent_status_bar_message
) )
self.tableView.loadMusicTableSignal.connect(self.load_media_playlist)
self.tableView.load_music_table() self.tableView.load_music_table()
self.player.playlistNextSignal.connect(self.on_next_clicked)
# playlistTreeView # playlistTreeView
self.playlistTreeView.playlistChoiceSignal.connect( self.playlistTreeView.playlistChoiceSignal.connect(
@ -316,6 +322,14 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# | | # | |
# |____________________| # |____________________|
def on_playlist_media_changed(self, media: QMediaContent):
"""Update stuff when the song changes"""
if not media.isNull():
file_url = media.canonicalUrl().toLocalFile()
metadata = id3_remap(get_tags(file_url)[0])
if metadata is not None:
self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"])
def on_volume_changed(self) -> None: def on_volume_changed(self) -> None:
"""Handles volume changes""" """Handles volume changes"""
try: try:
@ -351,15 +365,29 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
else: else:
self.playButton.setText("👽") self.playButton.setText("👽")
def on_previous_clicked(self) -> None: def on_prev_clicked(self) -> None:
"""""" """click previous - go to previous song"""
# TODO: implement this current_real_index = self.tableView.current_song_qmodel_index
debug("main.py on_previous_clicked()") index = self.tableView.proxymodel.mapFromSource(current_real_index)
row: int = index.row()
prev_row: int = row - 1
prev_index: QModelIndex = self.tableView.proxymodel.index(prev_row, index.column())
prev_filepath = prev_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data()
self.play_audio_file(prev_filepath)
self.tableView.set_current_song_qmodel_index(prev_index)
def on_next_clicked(self) -> None: def on_next_clicked(self) -> None:
"""""" """click next (or song ended) - go to next song"""
# TODO: implement this current_real_index = self.tableView.current_song_qmodel_index
debug("main.py on_next_clicked()") index = self.tableView.proxymodel.mapFromSource(current_real_index)
row: int = index.row()
next_row: int = row + 1
next_index: QModelIndex = self.tableView.proxymodel.index(next_row, index.column())
next_filepath = next_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data()
self.play_audio_file(next_filepath)
self.tableView.set_current_song_qmodel_index(next_index)
# ____________________ # ____________________
# | | # | |
@ -368,11 +396,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# | | # | |
# |____________________| # |____________________|
def load_media_playlist(self):
self.proxymodel.row
pass
# self.playlist.
def setup_fonts(self): def setup_fonts(self):
"""Initializes font sizes and behaviors for various UI components""" """Initializes font sizes and behaviors for various UI components"""
font: QFont = QFont() font: QFont = QFont()
@ -437,8 +460,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
""" """
if not filepath: if not filepath:
filepath = self.tableView.get_selected_song_filepath() filepath = self.tableView.get_selected_song_filepath()
# get metadata file_url = QUrl.fromLocalFile(filepath)
metadata = get_tags(filepath)[0] metadata = id3_remap(get_tags(filepath)[0])
# read the file # read the file
url = QUrl.fromLocalFile(filepath) url = QUrl.fromLocalFile(filepath)
# load the audio content # load the audio content
@ -451,14 +474,19 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
# assign "now playing" labels & album artwork # assign "now playing" labels & album artwork
if metadata is not None: if metadata is not None:
artist = metadata["TPE1"][0] if "TPE1" in metadata else None self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"], filepath)
album = metadata["TALB"][0] if "TALB" in metadata else None
title = metadata["TIT2"][0] if "TIT2" in metadata else None def set_ui_metadata(self, title, artist, album, filepath):
"""
Loads metadata into UI, presumably for current song
But you could pass any text here i guess
album art will always try to be current song
"""
self.artistLabel.setText(artist) self.artistLabel.setText(artist)
self.albumLabel.setText(album) self.albumLabel.setText(album)
self.titleLabel.setText(title) self.titleLabel.setText(title)
# set album artwork # set album artwork
album_art_data = self.tableView.get_current_song_album_art() album_art_data = get_album_art(filepath)
self.albumGraphicsView.load_album_art(album_art_data) self.albumGraphicsView.load_album_art(album_art_data)
def set_album_art_for_selected_songs(self, album_art_path: str) -> None: def set_album_art_for_selected_songs(self, album_art_path: str) -> None:

View File

@ -9,6 +9,7 @@ def get_album_art(file: str | None) -> bytes:
# Returns # Returns
bytes for album art or placeholder artwork bytes for album art or placeholder artwork
""" """
debug(f'try album art: {file}')
default_image_path = "./assets/default_album_art.jpg" default_image_path = "./assets/default_album_art.jpg"
if file: if file:
try: try:
@ -20,6 +21,7 @@ def get_album_art(file: str | None) -> bytes:
return audio.getall("APIC")[0].data return audio.getall("APIC")[0].data
except Exception as e: except Exception as e:
error(f"Error retrieving album art: {e}") error(f"Error retrieving album art: {e}")
return bytes()
with open(default_image_path, "rb") as f: with open(default_image_path, "rb") as f:
debug("loading placeholder album art") debug("loading placeholder album art")
return f.read() return f.read()