media player controls working
This commit is contained in:
parent
c3945c51b8
commit
466b38ce2e
@ -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
|
||||||
|
|||||||
@ -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!")
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
# map proxy (sortable) model to the original model (used for interactions)
|
index = self.currentIndex()
|
||||||
index = self.proxymodel.mapToSource(self.currentIndex())
|
# map proxy (sortable) model to the original model (used for interactions)
|
||||||
# set the proxy model index
|
model_index: QModelIndex = self.proxymodel.mapToSource(index)
|
||||||
self.current_song_qmodel_index: QModelIndex = index
|
self.current_song_qmodel_index: QModelIndex = model_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:
|
||||||
# map proxy (sortable) model to the original model (used for interactions)
|
index = self.currentIndex()
|
||||||
index = self.proxymodel.mapToSource(self.currentIndex())
|
# map proxy (sortable) model to the original model (used for interactions)
|
||||||
# set the proxy model index
|
model_index: QModelIndex = self.proxymodel.mapToSource(index)
|
||||||
self.selected_song_qmodel_index: QModelIndex = index
|
self.selected_song_qmodel_index: QModelIndex = model_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"""
|
||||||
|
|||||||
82
main.py
82
main.py
@ -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,15 +474,20 @@ 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):
|
||||||
self.artistLabel.setText(artist)
|
"""
|
||||||
self.albumLabel.setText(album)
|
Loads metadata into UI, presumably for current song
|
||||||
self.titleLabel.setText(title)
|
But you could pass any text here i guess
|
||||||
# set album artwork
|
album art will always try to be current song
|
||||||
album_art_data = self.tableView.get_current_song_album_art()
|
"""
|
||||||
self.albumGraphicsView.load_album_art(album_art_data)
|
self.artistLabel.setText(artist)
|
||||||
|
self.albumLabel.setText(album)
|
||||||
|
self.titleLabel.setText(title)
|
||||||
|
# set album artwork
|
||||||
|
album_art_data = get_album_art(filepath)
|
||||||
|
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:
|
||||||
"""Sets the ID3 tag APIC (album art) for all selected song filepaths"""
|
"""Sets the ID3 tag APIC (album art) for all selected song filepaths"""
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user