Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

29 changed files with 149 additions and 639 deletions

1
.gitignore vendored
View file

@ -1,5 +1,4 @@
wobuzz/settings.json wobuzz/settings.json
Wobuzz.egg-info Wobuzz.egg-info
build
__pycache__ __pycache__
.idea .idea

View file

@ -5,50 +5,26 @@ Currently, it just has really basic features but many more things are planned.
### Features ### Features
| Feature | Description | State | | Feature | Description | State |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| |---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> Implemented | | Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> Implemented |
| Background Job Monitor | A QDockWidget where background processes are listed. | <input type="checkbox" disabled checked /> Implemented | | Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | <input type="checkbox" disabled /> Not Implemented |
| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | <input type="checkbox" disabled /> Not Implemented |
| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | <input type="checkbox" disabled /> Not Implemented |
| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | <input type="checkbox" disabled /> Not Implemented |
| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | <input type="checkbox" disabled /> Not Implemented |
## Installation ## Installation
### Release installation To install Wobuzz, you firstly have to install the dependencies that can't be installed using pip.
This can be done using:
Look at the [Releases](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz/releases),
there you can find the commands that you need for the installation.
### Unstable git installation
You firstly have to install the newest dependencies:
``` bash ``` bash
sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip git sudo apt install pyqt6-dev-tools xcb libxcb-cursor0 ffmpeg
``` ```
Now, you can install the newest unstable version using just one more command: Now you can just clone the repo and let pip install it.
```bash
pip install wobuzz@git+https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git#egg=wobuzz
```
### Development installation
If you want to make changes to the code,
you can clone the repo and install it this time using the `-e` parameter,
which will tell pip to not copy the project to `~/.local/lib/python3.x/site-packages`,
but to create symlinks. \
Using this method, you can put the project wherever you want
(e.g. your Pycharm projects folder)
and the Python-module will always be in sync with the changes you do.
``` bash ``` bash
git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git
cd Wobuzz cd Wobuzz
pip install -e . pip install .
``` ```
## Usage: ## Usage:

View file

@ -1,5 +1,4 @@
PyQt6 PyQt6
pygame pygame
tinytag tinytag
pydub pydub
wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools

View file

@ -1,40 +1,29 @@
#!/usr/bin/python3 #!/usr/bin/python3
import setuptools import setuptools
import os
from pathlib import Path from pathlib import Path
import shutil
this_directory = Path(__file__).parent
desktop_entry_path = os.path.expanduser("~/.local/share/applications")
icon_path = os.path.expanduser("~/.local/share/icons/hicolor/scalable/apps")
os.makedirs(icon_path, exist_ok=True)
shutil.copy(f"{this_directory}/wobuzz.desktop", f"{desktop_entry_path}/wobuzz.desktop") # install desktop entry
shutil.copy(f"{this_directory}/wobuzz/icon.svg", f"{icon_path}/wobuzz.svg") # install icon
# use readme file as long description # use readme file as long description
this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text() long_description = (this_directory / "README.md").read_text()
setuptools.setup( setuptools.setup(
name="Wobuzz", name="Wobuzz",
version="0.1a2", version="0.0",
description="An audio player made by The Wobbler", description="An audio player made by The Wobbler",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz", url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz",
author="The Wobbler", author="The Wobbler",
author_email="emil@i21k.de", author_email="emil@i21k.de",
license="GNU GPLv3",
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]), packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
package_data={"": ["*.txt", "*.svg"]}, package_data={"": ["*.txt"]},
install_requires=[ install_requires=[
"PyQt6", "PyQt6",
"tinytag", "tinytag",
"pydub", "pydub",
"pygame", "pygame",
"wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools" "wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@main#egg=wobbl_tools"
], ],
entry_points={ entry_points={
"console_scripts": ["wobuzz=wobuzz.command_line:main"], "console_scripts": ["wobuzz=wobuzz.command_line:main"],

View file

@ -1,10 +0,0 @@
[Desktop Entry]
Type=Application
Name=Wobuzz
Comment=A simple audio player
Keywords=audio;music;player;wobuzz;wobbl;qt;
Categories=Multimedia;Media;AudioVideo;Audio;Player;Music;Qt;
Exec=wobuzz %F
Icon=wobuzz
Terminal=false
MimeType=audio/mpeg;audio/x-wav;audio/x-flac;audio/x-aiff;audio/x-m4a;audio/x-ms-wma;audio/x-vorbis+ogg;

View file

@ -4,8 +4,6 @@ import os
import sys import sys
import argparse import argparse
from wobuzz.player.playlist import Playlist
def main(): def main():
description = "A music player made by The Wobbler." description = "A music player made by The Wobbler."
@ -22,24 +20,23 @@ def main():
app = Wobuzz() app = Wobuzz()
if arguments.playlist: if arguments.playlist:
playlist = Playlist(app, "Temporary Playlist", arguments.playlist) app.library.temporary_playlist.clear()
app.library.temporary_playlist.view.clear()
app.library.playlists.append(playlist) app.library.temporary_playlist.load_from_m3u(arguments.playlist)
app.library.temporary_playlist.view.load_tracks()
if app.library.temporary_playlist in app.library.playlists:
app.library.playlists.remove(app.library.temporary_playlist)
app.library.temporary_playlist = playlist
if arguments.track: if arguments.track:
playlist = Playlist(app, "Temporary Playlist", arguments.track) app.library.temporary_playlist.clear()
app.library.temporary_playlist.view.clear()
app.library.playlists.append(playlist) # make track paths absolute
tracks = []
if app.library.temporary_playlist in app.library.playlists: for track in arguments.track:
app.library.playlists.remove(app.library.temporary_playlist) tracks.append(os.path.abspath(track))
app.library.temporary_playlist = playlist
app.library.load_playlist_views() app.library.temporary_playlist.load_from_paths(tracks)
app.library.temporary_playlist.view.load_tracks()
sys.exit(app.qt_app.exec()) sys.exit(app.qt_app.exec())

View file

@ -11,13 +11,13 @@ class GUI:
self.dropped = [] self.dropped = []
self.clicked_playlist = self.app.library.temporary_playlist
self.window = MainWindow(app) self.window = MainWindow(app)
self.settings = self.window.settings self.settings = self.window.settings
self.track_control = self.window.track_control self.track_control = self.window.track_control
self.process_dock = self.window.process_dock
self.track_info = self.window.track_info
self.window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.app.library.main_library_dock) self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.app.library.main_library_dock)
self.app.library.main_library_dock.setFeatures( self.app.library.main_library_dock.setFeatures(
QDockWidget.DockWidgetFeature.DockWidgetMovable | QDockWidget.DockWidgetFeature.DockWidgetMovable |
@ -52,18 +52,5 @@ class GUI:
def on_track_change(self, previous_track, track): def on_track_change(self, previous_track, track):
self.track_control.on_track_change(previous_track, track) self.track_control.on_track_change(previous_track, track)
self.app.player.current_playlist.view.on_track_change(previous_track, track)
for dock_id in self.app.player.current_playlist.views:
view = self.app.player.current_playlist.views[dock_id]
view.on_track_change(previous_track, track)
def on_background_job_start(self, job_name: str, description: str, steps: int=0, getter: any=None):
self.process_dock.job_started_signal.emit(job_name, description, steps, getter)
def on_background_job_stop(self, job_name: str):
self.process_dock.job_finished_signal.emit(job_name)
def on_playstate_update(self):
self.track_control.on_playstate_update()
self.track_info.update_info()

View file

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93333"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="0.75989759"
inkscape:cx="421.10938"
inkscape:cy="458.61443"
inkscape:window-width="1920"
inkscape:window-height="1023"
inkscape:window-x="0"
inkscape:window-y="1075"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#6d758a;fill-opacity:1;stroke:none;stroke-width:0.367058px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 182.77204,41.828827 c 26.1142,-31.9117865 53.72304,-45.9799062 62.67325,-40.7688755 8.53202,4.9675519 4.87612,41.4855685 -14.96576,76.6633855 -5.53567,9.814241 -55.03746,-26.937222 -47.70749,-35.89451 z"
id="path6997"
sodipodi:nodetypes="ssss" />
<path
style="fill:#6d758a;fill-opacity:1;stroke:none;stroke-width:0.367058px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 88.161294,41.828827 C 62.047094,9.9170405 34.438258,-4.1510792 25.488049,1.0599515 16.95603,6.0275034 20.611925,42.54552 40.453804,77.723337 c 5.535677,9.814241 55.03746,-26.937222 47.70749,-35.89451 z"
id="path447"
sodipodi:nodetypes="ssss" />
<path
style="fill:#a0abc7;fill-opacity:1;stroke:none;stroke-width:0.176989px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 66.317189,37.01117 C 53.668122,21.693513 40.295092,14.940816 35.959835,17.44211 31.827139,19.826534 33.597962,37.355182 43.208873,54.240535 45.890217,58.951371 69.867642,41.310669 66.317189,37.01117 Z"
id="path2506"
sodipodi:nodetypes="ssss" />
<circle
style="fill:#141414;fill-opacity:1;stroke:none;stroke-width:0.338083;stroke-opacity:1"
id="path234"
cx="135.46666"
cy="151.87083"
r="119.0625" />
<circle
style="fill:#ffbf00;fill-opacity:1;stroke:none;stroke-width:0.22539;stroke-opacity:1"
id="circle2604"
cx="135.46666"
cy="151.87083"
r="79.375" />
<circle
style="fill:#141414;fill-opacity:1;stroke:none;stroke-width:0.112695;stroke-opacity:1"
id="circle2602"
cx="135.46666"
cy="151.87083"
r="39.6875" />
<path
style="fill:#a0abc7;fill-opacity:1;stroke:none;stroke-width:0.176989px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 204.61614,37.01117 c 12.64906,-15.317657 26.02209,-22.070354 30.35735,-19.56906 4.1327,2.384424 2.36187,19.913072 -7.24904,36.798425 -2.68134,4.710836 -26.65877,-12.929866 -23.10831,-17.229365 z"
id="path295"
sodipodi:nodetypes="ssss" />
</g>
<metadata
id="metadata344">
<rdf:RDF>
<cc:Work
rdf:about="" />
</rdf:RDF>
</metadata>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
from PyQt6.QtWidgets import QTabWidget, QAbstractItemView from PyQt6.QtWidgets import QTabWidget
from ..player.playlist import Playlist from ..player.playlist import Playlist
from ..ui.library_dock import LibraryDock from ..ui.library_dock import LibraryDock
from ..ui.playlist import PlaylistView from ..ui.playlist import PlaylistView
@ -18,13 +18,10 @@ class Library:
self.main_library_dock = LibraryDock(self) self.main_library_dock = LibraryDock(self)
self.library_docks = [self.main_library_dock] self.library_docks = [self.main_library_dock]
self.playlists = [] self.temporary_playlist = Playlist(self.app, "Temporary Playlist")
self.temporary_playlist = None self.playlists = [self.temporary_playlist]
def load(self): def load(self):
self.load_playlists()
def load_playlists(self):
path_playlists = os.path.expanduser(f"{self.app.settings.library_path}/playlists") path_playlists = os.path.expanduser(f"{self.app.settings.library_path}/playlists")
if not os.path.exists(path_playlists): if not os.path.exists(path_playlists):
@ -40,11 +37,16 @@ class Library:
if file_name.endswith(".m3u"): if file_name.endswith(".m3u"):
path = f"{path_playlists}/{file_name}" path = f"{path_playlists}/{file_name}"
playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0]) if file_name == "Temporary_Playlist.wbz.m3u":
self.playlists.append(playlist) playlist = self.temporary_playlist
if playlist.title == "Temporary Playlist": else:
self.temporary_playlist = playlist playlist = Playlist(self.app, file_name.replace("_", " ").split(".")[0])
self.playlists.append(playlist)
playlist.load_from_m3u(path)
self.load_playlist_views()
def load_playlist_views(self): def load_playlist_views(self):
for library_dock in self.library_docks: for library_dock in self.library_docks:
@ -53,35 +55,20 @@ class Library:
playlist_tabs.playlists = {} playlist_tabs.playlists = {}
for playlist in self.playlists: for playlist in self.playlists:
playlist_view = PlaylistView(playlist, library_dock) playlist_view = PlaylistView(playlist)
playlist_tabs.addTab(playlist_view, playlist.title) playlist_tabs.addTab(playlist_view, playlist.title)
if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened
playlist_tabs.setCurrentIndex(playlist_tabs.count() - 1)
if self.app.settings.load_on_start:
for playlist in self.playlists:
playlist.load()
def on_exit(self, event): def on_exit(self, event):
for playlist in self.playlists: for playlist in self.playlists:
if playlist.loaded: # only save loaded playlists, unloaded are empty playlist.save()
playlist.save()
if self.app.player.current_playlist is not None:
self.app.settings.latest_playlist = self.app.player.current_playlist.path
def new_playlist(self): def new_playlist(self):
playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist")) playlist = Playlist(self.app, self.app.utils.unique_name("New Playlist"))
playlist.loaded = True
self.playlists.append(playlist) self.playlists.append(playlist)
for library_dock in self.library_docks: for library_dock in self.library_docks:
playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs
playlist_view = PlaylistView(playlist, library_dock) playlist_view = PlaylistView(playlist)
playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop
playlist_tabs.addTab(playlist_view, playlist.title) playlist_tabs.addTab(playlist_view, playlist.title)

View file

@ -1,5 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os
import sys import sys
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from wobbl_tools.data_file import load_dataclass_json from wobbl_tools.data_file import load_dataclass_json
@ -19,14 +20,14 @@ class Wobuzz:
self.settings = load_dataclass_json(Settings, self.utils.settings_location, self, True, True) self.settings = load_dataclass_json(Settings, self.utils.settings_location, self, True, True)
self.settings.set_attribute_change_event(self.on_settings_change) self.settings.set_attribute_change_event(self.on_settings_change)
self.library = Library(self)
self.player = Player(self) self.player = Player(self)
self.library = Library(self)
self.gui = GUI(self) self.gui = GUI(self)
self.late_init() self.post_init()
def late_init(self): def post_init(self):
self.gui.track_control.track_progress_slider.late_init() self.gui.track_control.track_progress_slider.post_init()
self.library.load() self.library.load()
def on_settings_change(self, key, value): def on_settings_change(self, key, value):
@ -37,7 +38,4 @@ class Wobuzz:
if __name__ == "__main__": if __name__ == "__main__":
wobuzz = Wobuzz() wobuzz = Wobuzz()
wobuzz.post_init()
wobuzz.library.load_playlist_views()
sys.exit(wobuzz.qt_app.exec()) sys.exit(wobuzz.qt_app.exec())

View file

@ -1,6 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
import time
import threading import threading
import pygame.mixer import pygame.mixer
import pygame.event import pygame.event
@ -19,7 +18,7 @@ class Player:
self.track_progress = TrackProgress(self.app) self.track_progress = TrackProgress(self.app)
self.history = Playlist(self.app, "History") self.history = Playlist(self.app, "History")
self.current_playlist = None self.current_playlist = Playlist(self.app, "None")
self.playing = False self.playing = False
self.paused = False self.paused = False
@ -33,7 +32,7 @@ class Player:
self.playing = True self.playing = True
self.paused = False self.paused = False
self.app.gui.on_playstate_update() self.app.gui.track_control.on_playstate_update()
# cache next track so it immediately starts when the current track finishes # cache next track so it immediately starts when the current track finishes
self.cache_next_track() self.cache_next_track()
@ -68,7 +67,7 @@ class Player:
self.app.gui.on_track_change(self.history.h_last_track(), self.current_playlist.current_track) self.app.gui.on_track_change(self.history.h_last_track(), self.current_playlist.current_track)
self.app.gui.on_playstate_update() self.app.gui.track_control.on_playstate_update()
def play_track_in_playlist(self, track_index): def play_track_in_playlist(self, track_index):
self.stop() self.stop()
@ -99,7 +98,7 @@ class Player:
self.track_progress.pause() self.track_progress.pause()
self.paused = True self.paused = True
self.app.gui.on_playstate_update() self.app.gui.track_control.on_playstate_update()
def unpause(self): def unpause(self):
self.music_channel.unpause() self.music_channel.unpause()
@ -108,7 +107,7 @@ class Player:
self.playing = True self.playing = True
self.paused = False self.paused = False
self.app.gui.on_playstate_update() self.app.gui.track_control.on_playstate_update()
def next_track(self): def next_track(self):
if not self.current_playlist.on_last_track(): if not self.current_playlist.on_last_track():
@ -135,13 +134,13 @@ class Player:
self.music_channel.stop() self.music_channel.stop()
self.track_progress.stop() self.track_progress.stop()
if self.current_playlist is not None and self.current_playlist.current_track is not None: if not self.current_playlist.current_track is None:
self.current_sound_duration = self.current_playlist.current_track.duration self.current_sound_duration = self.current_playlist.current_track.duration
self.playing = False self.playing = False
self.paused = False self.paused = False
self.app.gui.on_playstate_update() self.app.gui.track_control.on_playstate_update()
def seek(self, position: int): def seek(self, position: int):
self.music_channel.stop() self.music_channel.stop()
@ -161,15 +160,8 @@ class Player:
track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1] track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1]
if not track.cached: if not track.cached:
self.app.gui.on_background_job_start(
"Loading Track",
"Loading next track in the background so it starts immediately."
)
track.cache() track.cache()
self.app.gui.on_background_job_stop("Loading Track")
def cache_next_track(self): def cache_next_track(self):
# function that creates a thread which will cache the next track # function that creates a thread which will cache the next track
caching_thread = threading.Thread(target=self.caching_thread_function) caching_thread = threading.Thread(target=self.caching_thread_function)
@ -178,15 +170,6 @@ class Player:
def start_playlist(self, playlist): def start_playlist(self, playlist):
self.stop() self.stop()
if not playlist.loaded:
playlist.load()
while not playlist.has_tracks() and not playlist.loaded: # wait until first track is loaded
time.sleep(0.1)
if not playlist.has_tracks():
return
self.current_sound, self.current_sound_duration = playlist.set_track(0) # first track self.current_sound, self.current_sound_duration = playlist.set_track(0) # first track
self.current_playlist = playlist self.current_playlist = playlist

View file

@ -1,23 +1,15 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import threading
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QAbstractItemView
from .track import Track from .track import Track
class Playlist: class Playlist:
def __init__(self, app, title: str, load_from=None): def __init__(self, app, title: str):
self.app = app self.app = app
self.title = title # playlist title self.title = title # playlist title
# if the playlist is imported and not already in the library, this variable will contain the playlist path or
# track path from which the playlist will get imported
# if None, playlist should be already in the library and will be loaded from a .wbz.m3u
self.load_from = load_from
# add to unique names so if the playlist is loaded from disk, # add to unique names so if the playlist is loaded from disk,
# no other playlist can be created using the same name # no other playlist can be created using the same name
self.app.utils.unique_names.append(self.title) self.app.utils.unique_names.append(self.title)
@ -26,12 +18,7 @@ class Playlist:
self.tracks: list[Track] = [] self.tracks: list[Track] = []
self.current_track_index = 0 self.current_track_index = 0
self.current_track: Track | None = None self.current_track: Track | None = None
self.views = {} # dict of id(LibraryDock): PlaylistView self.view = None
self.loaded = False
self.path = os.path.expanduser(
f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
)
def clear(self): def clear(self):
self.sorting: list[Qt.SortOrder] | None = None self.sorting: list[Qt.SortOrder] | None = None
@ -40,49 +27,19 @@ class Playlist:
self.current_track = None self.current_track = None
def load_from_paths(self, paths): def load_from_paths(self, paths):
num_tracks = len(paths)
i = 0 i = 0
process_title = f'Loading Playlist "{self.title}"' while i < len(paths):
self.app.gui.on_background_job_start(
process_title,
f'Loading the tracks of "{self.title}".',
num_tracks,
lambda: i
)
while i < num_tracks:
path = paths[i] path = paths[i]
if os.path.isfile(path): if os.path.isfile(path):
self.append_track(Track(self.app, path, cache=i==0)) # first track is cached self.tracks.append(Track(self.app, path, cache=i==0)) # first track is cached
i += 1 i += 1
self.loaded = True # set current track to the first track if there is no currently playing track
if self.current_track is None and self.has_tracks():
self.app.gui.on_background_job_stop(process_title) self.current_track = self.tracks[0]
def load(self):
loading_thread = threading.Thread(target=self.loading_thread)
loading_thread.start()
def loading_thread(self):
if self.load_from is None: # if the playlist is in the library
self.load_from_wbz(self.path)
elif isinstance(self.load_from, str): # if it's imported from a .m3u
self.load_from_m3u(self.load_from)
elif isinstance(self.load_from, list): # if it's created from tracks
self.load_from_paths(self.load_from)
for dock_id in self.views: # enable drag and drop on every view
view = self.views[dock_id]
view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
def load_from_m3u(self, path): def load_from_m3u(self, path):
file = open(path, "r") file = open(path, "r")
@ -92,18 +49,8 @@ class Playlist:
lines = m3u.split("\n") # m3u entries are separated by newlines lines = m3u.split("\n") # m3u entries are separated by newlines
lines = lines[:-1] # remove last entry because it is just an empty string lines = lines[:-1] # remove last entry because it is just an empty string
num_lines = len(lines)
i = 0 i = 0
num_lines = len(lines)
process_title = f'Loading Playlist "{self.title}"'
self.app.gui.on_background_job_start(
process_title,
f'Loading the tracks of "{self.title}".',
num_lines,
lambda: i
)
while i < num_lines: while i < num_lines:
line = lines[i] line = lines[i]
@ -113,7 +60,7 @@ class Playlist:
continue continue
self.append_track(Track(self.app, line, cache=i==0)) # first track is cached self.tracks.append(Track(self.app, line, cache=i==0)) # first track is cached
i += 1 i += 1
@ -121,12 +68,10 @@ class Playlist:
if self.current_track is None and self.has_tracks(): if self.current_track is None and self.has_tracks():
self.current_track = self.tracks[0] self.current_track = self.tracks[0]
self.loaded = True #self.app.player.history.append_track(self.current_track)
self.app.gui.on_background_job_stop(process_title)
def load_from_wbz(self, path): def load_from_wbz(self, path):
self.load_from_m3u(path) # placeholder pass
def has_tracks(self): def has_tracks(self):
return len(self.tracks) > 0 return len(self.tracks) > 0
@ -174,44 +119,46 @@ class Playlist:
for track in self.tracks: for track in self.tracks:
wbz_data += f"{track.path}\n" wbz_data += f"{track.path}\n"
wbz = open(self.path, "w") wbz = open(
os.path.expanduser(
f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
),
"w"
)
wbz.write(wbz_data) wbz.write(wbz_data)
wbz.close() wbz.close()
def rename(self, title: str): def rename(self, title: str):
# remove from unique names so a new playlist can have the old name and delete old playlist. # remove from unique names so a new playlist can have the old name and delete old playlist.
if os.path.exists(self.path): path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
os.remove(self.path) path = os.path.expanduser(path)
if os.path.exists(path):
os.remove(os.path.expanduser(path))
old_title = self.title old_title = self.title
self.title = self.app.utils.unique_name(title, ignore=old_title) self.title = self.app.utils.unique_name(title, ignore=old_title)
self.path = os.path.expanduser(
f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
)
if not old_title == self.title: # remove only when the playlist actually has a different name if not old_title == self.title: # remove only when the playlist actually has a different name
self.app.utils.unique_names.remove(old_title) self.app.utils.unique_names.remove(old_title)
def delete(self): def delete(self):
if os.path.exists(self.path): path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
os.remove(self.path) path = os.path.expanduser(path)
if os.path.exists(path):
os.remove(os.path.expanduser(path))
self.app.utils.unique_names.remove(self.title) self.app.utils.unique_names.remove(self.title)
self.app.library.playlists.remove(self) self.app.library.playlists.remove(self)
if self.app.player.current_playlist == self: # stop if this is the current playlist
self.app.player.stop()
self.app.player.current_playlist = None
def append_track(self, track): def append_track(self, track):
for dock_id in self.views:
view = self.views[dock_id]
view.append_track(track)
self.tracks.append(track) self.tracks.append(track)
if self.view:
self.view.append_track(track)
def h_last_track(self): def h_last_track(self):
# get last track in history (only gets used in player.history) # get last track in history (only gets used in player.history)

View file

@ -1,20 +1,29 @@
#!/usr/bin/python3 #!/usr/bin/python3
from pydub import AudioSegment from pydub import AudioSegment
from pydub.effects import normalize
from pygame.mixer import Sound from pygame.mixer import Sound
from tinytag import TinyTag from tinytag import TinyTag
SUPPORTED_FORMATS = [
"mp3",
"wav",
"ogg"
]
class Track: class Track:
""" """
Class containing data for a track like file path, raw data... Class containing data for a track like file path, raw data...
""" """
def __init__(self, app, path: str, cache: bool=False): def __init__(self, app, path: str, property_string: str=None, cache: bool=False):
self.app = app self.app = app
self.path = path self.path = path
self.property_string = property_string
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) self.tags = TinyTag.get(self.path, ignore_errors=False, duration=False)
self.cached = False self.cached = False
self.audio = None self.audio = None
@ -33,7 +42,6 @@ class Track:
new_occurrences = {} new_occurrences = {}
for item in self.items: for item in self.items:
# create dict of item: item.index (actually the id of the item bc. the item can't be used as key)
playlist_occurrences = new_occurrences.get(item.playlist, {}) playlist_occurrences = new_occurrences.get(item.playlist, {})
playlist_occurrences[id(item)] = item.index playlist_occurrences[id(item)] = item.index
@ -48,14 +56,13 @@ class Track:
If this track is the currently playing track, and it gets moved, this corrects the current playlist index. If this track is the currently playing track, and it gets moved, this corrects the current playlist index.
""" """
if self.app.player.current_playlist is not None: if self.app.player.current_playlist.current_track is self:
if self.app.player.current_playlist.current_track is self: for item in self.items:
for item in self.items: if (
if ( item.playlist in self.occurrences and
item.playlist in self.occurrences and self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index
self.occurrences[item.playlist][id(item)] == self.app.player.current_playlist.current_track_index ):
): self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)])
self.app.player.current_playlist.set_track(new_occurrences[item.playlist][id(item)])
def cache(self): def cache(self):
self.load_audio() self.load_audio()
@ -67,8 +74,6 @@ class Track:
self.duration = len(self.audio) # track duration in milliseconds self.duration = len(self.audio) # track duration in milliseconds
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images
self.cached = True self.cached = True
def clear_cache(self): def clear_cache(self):
@ -78,12 +83,11 @@ class Track:
self.sound = None self.sound = None
self.duration = 0 self.duration = 0
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images
def load_audio(self): def load_audio(self):
#file_type = self.path.split(".")[-1] file_type = self.path.split(".")[-1]
self.audio = AudioSegment.from_file(self.path) if file_type in SUPPORTED_FORMATS:
self.audio = AudioSegment.from_file(self.path)
def remaining(self, position: int): def remaining(self, position: int):
remaining_audio = self.audio[position:] remaining_audio = self.audio[position:]

View file

@ -30,5 +30,5 @@ class TrackProgress:
def stop(self): def stop(self):
self.timer.stop() self.timer.stop()
if self.app.player.current_playlist is not None and self.app.player.current_playlist.current_track is not None: if not self.app.player.current_playlist.current_track is None:
self.remaining_time = self.app.player.current_playlist.current_track.duration self.remaining_time = self.app.player.current_playlist.current_track.duration

View file

@ -9,6 +9,4 @@ class Settings:
window_maximized: bool=False window_maximized: bool=False
library_path: str="~/.wobuzz" library_path: str="~/.wobuzz"
clear_track_cache: bool=True clear_track_cache: bool=True
latest_playlist: str=None
load_on_start: bool=True

View file

@ -11,6 +11,12 @@ class LibraryDock(QDockWidget):
self.library = library self.library = library
self.setAllowedAreas(
Qt.DockWidgetArea.LeftDockWidgetArea |
Qt.DockWidgetArea.RightDockWidgetArea |
Qt.DockWidgetArea.BottomDockWidgetArea
)
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.library_widget = Library(library, self) self.library_widget = Library(library, self)

View file

@ -1,12 +1,9 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMainWindow, QMenu from PyQt6.QtWidgets import QMainWindow, QMenu
from .track_control import TrackControl from .track_control import TrackControl
from .settings import Settings from .settings import Settings
from .process.process_dock import ProcessDock
from .track_info import TrackInfo
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
@ -15,10 +12,7 @@ class MainWindow(QMainWindow):
self.app = app self.app = app
self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg")
self.setWindowTitle("Wobuzz") self.setWindowTitle("Wobuzz")
self.setWindowIcon(self.icon)
self.menu_bar = self.menuBar() self.menu_bar = self.menuBar()
@ -30,11 +24,6 @@ class MainWindow(QMainWindow):
self.settings_action = self.edit_menu.addAction("&Settings") self.settings_action = self.edit_menu.addAction("&Settings")
self.view_menu = QMenu("&View", self.menu_bar)
self.menu_bar.addMenu(self.view_menu)
self.processes_action = self.view_menu.addAction("Show &Background Processes")
self.track_control = TrackControl(app) self.track_control = TrackControl(app)
self.addToolBar(self.track_control) self.addToolBar(self.track_control)
@ -42,13 +31,5 @@ class MainWindow(QMainWindow):
self.settings.hide() self.settings.hide()
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.settings) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.settings)
self.process_dock = ProcessDock(app)
self.process_dock.hide()
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.process_dock)
self.track_info = TrackInfo(app)
self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.track_info)
self.settings_action.triggered.connect(self.settings.show) self.settings_action.triggered.connect(self.settings.show)
self.processes_action.triggered.connect(self.process_dock.show)

View file

@ -2,7 +2,7 @@
from PyQt6.QtCore import pyqtSignal from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QDropEvent, QIcon, QFont from PyQt6.QtGui import QDropEvent, QIcon, QFont
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame
from .track import TrackItem from .track import TrackItem
@ -10,22 +10,25 @@ from .track import TrackItem
class PlaylistView(QTreeWidget): class PlaylistView(QTreeWidget):
itemDropped = pyqtSignal(QTreeWidget, list) itemDropped = pyqtSignal(QTreeWidget, list)
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) def __init__(self, playlist, parent=None):
def __init__(self, playlist, dock, parent=None):
super().__init__(parent) super().__init__(parent)
self.playlist = playlist self.playlist = playlist
self.library_dock = dock
self.app = playlist.app self.app = playlist.app
playlist.views[id(dock)] = self playlist.view = self
self.normal_font = QFont()
self.bold_font = QFont()
self.bold_font.setBold(True)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setColumnCount(4) self.setColumnCount(4)
self.playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
headers = [ headers = [
"#", "#",
"Title", "Title",
@ -36,6 +39,8 @@ class PlaylistView(QTreeWidget):
self.setHeaderLabels(headers) self.setHeaderLabels(headers)
self.load_tracks()
self.itemActivated.connect(self.on_track_activation) self.itemActivated.connect(self.on_track_activation)
def on_user_sort(self): def on_user_sort(self):
@ -58,8 +63,7 @@ class PlaylistView(QTreeWidget):
i += 1 i += 1
# make sure the next track is cached (could be moved by user) if self.app.player.current_playlist.has_tracks():
if self.app.player.current_playlist == self.playlist and self.app.player.current_playlist.has_tracks():
self.app.player.cache_next_track() self.app.player.cache_next_track()
def dropEvent(self, event: QDropEvent): def dropEvent(self, event: QDropEvent):
@ -129,14 +133,20 @@ class PlaylistView(QTreeWidget):
# unmark the previous track in all playlists # unmark the previous track in all playlists
for item in previous_track.items: for item in previous_track.items:
item.unmark() item.setIcon(0, QIcon(None))
item.setFont(1, self.normal_font)
item.setFont(2, self.normal_font)
item.setFont(3, self.normal_font)
if track: if track:
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
# mark the current track in this playlist # mark the current track in this playlist
item = self.topLevelItem(self.app.player.current_playlist.current_track_index) item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
item.mark() item.setIcon(0, self.playing_mark)
item.setFont(1, self.bold_font)
item.setFont(2, self.bold_font)
item.setFont(3, self.normal_font)
def append_track(self, track): def append_track(self, track):
TrackItem(track, self.topLevelItemCount() - 1, self) TrackItem(track, self.topLevelItemCount() - 1, self)

View file

@ -36,10 +36,6 @@ class PlaylistTabBar(QTabBar):
def on_doubleclick(self, index): def on_doubleclick(self, index):
playlist_view = self.tab_widget.widget(index) playlist_view = self.tab_widget.widget(index)
if playlist_view is None: # dont crash if no playlist was double-clicked
return
playlist = playlist_view.playlist playlist = playlist_view.playlist
self.app.player.start_playlist(playlist) self.app.player.start_playlist(playlist)

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QMouseEvent, QCursor from PyQt6.QtGui import QMouseEvent
from PyQt6.QtWidgets import QLineEdit from PyQt6.QtWidgets import QLineEdit
from .tab_bar import PlaylistTabBar from .tab_bar import PlaylistTabBar
@ -16,10 +16,9 @@ class TabTitle(QLineEdit):
self.index = index self.index = index
self.playlist_view = playlist_view self.playlist_view = playlist_view
self.setStyleSheet("QLineEdit {background: transparent; border: none;}") self.setStyleSheet("QLineEdit {background: transparent;}")
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor) self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
self.editingFinished.connect(self.on_edit) self.editingFinished.connect(self.on_edit)

View file

@ -1 +0,0 @@
#!/usr/bin/python3

View file

@ -1,41 +0,0 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QSizePolicy, QGroupBox, QLabel, QProgressBar, QVBoxLayout
class BackgroundProcess(QGroupBox):
normal_font = QFont()
normal_font.setBold(False)
bold_font = QFont()
bold_font.setBold(True)
def __init__(self, title: str, parent=None, description: str="", steps: int=0):
super().__init__(title, parent)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter)
self.setFont(self.bold_font)
self.layout = QVBoxLayout(self)
self.description = QLabel(description, self)
self.description.setFont(self.normal_font)
self.layout.addWidget(self.description)
self.progress_bar = QProgressBar(self)
self.progress_bar.setMaximum(steps)
self.layout.addWidget(self.progress_bar)
self.setLayout(self.layout)
def get_progress(self):
return 0
def set_range(self, maximum: int, minimum: int=0):
self.progress_bar.setRange(minimum, maximum)
def update_progress(self):
self.progress_bar.setValue(self.get_progress())

View file

@ -1,75 +0,0 @@
#!/usr/bin/python3
from PyQt6.QtCore import QTimer, pyqtSignal
from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout
from .process import BackgroundProcess
PROGRESS_UPDATE_RATE = 10
PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE
class ProcessDock(QDockWidget):
# we need a signal for self.on_background_job_start() because PyQt6 doesn't allow some operations to be performed
# from a different thread
job_started_signal = pyqtSignal(str, str, int, object)
job_finished_signal = pyqtSignal(str)
def __init__(self, app, parent=None):
super().__init__(parent)
self.app = app
self.processes = {}
self.setWindowTitle("Background Processes")
self.scroll_area = QScrollArea(self)
self.scroll_area.setWidgetResizable(True)
self.process_container = QWidget(self.scroll_area)
self.process_layout = QVBoxLayout(self.process_container)
# add expanding widget so the distance between processes will be equal
self.process_layout.addWidget(QWidget(self.process_container))
self.process_container.setLayout(self.process_layout)
self.scroll_area.setWidget(self.process_container)
self.setWidget(self.scroll_area)
self.progress_update_timer = QTimer()
self.progress_update_timer.timeout.connect(self.update_processes)
self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL)
self.job_started_signal.connect(self.on_background_job_start)
self.job_finished_signal.connect(self.on_background_job_stop)
def add_process(self, name: str, process: BackgroundProcess):
if not name in self.processes:
self.processes[name] = process
self.process_layout.insertWidget(self.process_layout.count() - 1, process)
def update_processes(self):
for process in self.processes.values():
process.update_progress()
def on_background_job_start(self, job_title: str, description: str, steps: int, getter):
process = BackgroundProcess(
job_title,
self.process_container,
description,
steps
)
if getter is not None:
process.get_progress = getter
self.add_process(job_title, process)
def on_background_job_stop(self, job):
if job in self.processes:
self.processes.pop(job).deleteLater()

View file

@ -10,8 +10,5 @@ class BehaviourSettings(QWidget):
self.layout = QFormLayout(self) self.layout = QFormLayout(self)
self.setLayout(self.layout) self.setLayout(self.layout)
self.load_on_start = QCheckBox(self)
self.layout.addRow("Load playlists on start", self.load_on_start)
self.clear_track_cache = QCheckBox(self) self.clear_track_cache = QCheckBox(self)
self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache) self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache)

View file

@ -44,7 +44,6 @@ class Settings(QDockWidget):
def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event
self.file_settings.library_path_input.setText(self.app.settings.library_path) self.file_settings.library_path_input.setText(self.app.settings.library_path)
self.behavior_settings.clear_track_cache.setChecked(self.app.settings.clear_track_cache) self.behavior_settings.clear_track_cache.setChecked(self.app.settings.clear_track_cache)
self.behavior_settings.load_on_start.setChecked(self.app.settings.load_on_start)
def update_settings(self, key, value): def update_settings(self, key, value):
match key: match key:
@ -54,11 +53,7 @@ class Settings(QDockWidget):
case "clear_track_cache": case "clear_track_cache":
self.behavior_settings.clear_track_cache.setDown(value) self.behavior_settings.clear_track_cache.setDown(value)
case "load_on_start":
self.behavior_settings.load_on_start.setChecked(value)
def write_settings(self): def write_settings(self):
self.app.settings.library_path = self.file_settings.library_path_input.text() self.app.settings.library_path = self.file_settings.library_path_input.text()
self.app.settings.clear_track_cache = self.behavior_settings.clear_track_cache.isChecked() self.app.settings.clear_track_cache = self.behavior_settings.clear_track_cache.isChecked()
self.app.settings.load_on_start = self.behavior_settings.load_on_start.isChecked()

View file

@ -1,31 +1,20 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QIcon, QPalette
from PyQt6.QtWidgets import QTreeWidgetItem from PyQt6.QtWidgets import QTreeWidgetItem
class TrackItem(QTreeWidgetItem): class TrackItem(QTreeWidgetItem):
normal_font = QFont()
bold_font = QFont()
bold_font.setBold(True)
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
def __init__(self, track, index, parent=None): def __init__(self, track, index, parent=None):
super().__init__(parent) super().__init__(parent)
self.track = track self.track = track
self.index_user_sort = index self.index_user_sort = index
self.index = index self.index = index
self.playlist = parent.playlist self.playlist = parent.playlist
palette = parent.palette()
self.highlight_color = palette.color(QPalette.ColorRole.Highlight)
self.base_color = palette.color(QPalette.ColorRole.Base)
track.items.append(self) track.items.append(self)
track.set_occurrences() track.set_occurrences()
@ -42,16 +31,3 @@ class TrackItem(QTreeWidgetItem):
self.setText(3, track.tags.album) self.setText(3, track.tags.album)
self.setText(4, str(self.index_user_sort + 1)) self.setText(4, str(self.index_user_sort + 1))
def mark(self):
self.setIcon(0, self.playing_mark)
self.setFont(1, self.bold_font)
self.setFont(2, self.bold_font)
self.setFont(3, self.normal_font)
def unmark(self):
self.setIcon(0, QIcon(None))
self.setFont(1, self.normal_font)
self.setFont(2, self.normal_font)
self.setFont(3, self.normal_font)

View file

@ -46,15 +46,15 @@ class TrackControl(QToolBar):
self.next_button.triggered.connect(self.next_track) self.next_button.triggered.connect(self.next_track)
def previous_track(self): def previous_track(self):
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): if self.app.player.current_playlist.has_tracks():
self.app.player.previous_track() self.app.player.previous_track()
def stop(self): def stop(self):
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): if self.app.player.current_playlist.has_tracks():
self.app.player.stop() self.app.player.stop()
def next_track(self): def next_track(self):
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): if self.app.player.current_playlist.has_tracks():
self.app.player.next_track() self.app.player.next_track()
def on_track_change(self, previous_track, track): def on_track_change(self, previous_track, track):
@ -80,17 +80,11 @@ class TrackControl(QToolBar):
elif self.app.player.playing: # playing elif self.app.player.playing: # playing
self.app.player.pause() self.app.player.pause()
# stopped but tracks in the current playlist elif self.app.player.current_playlist.has_tracks(): # stopped but tracks in the current playlist
elif self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
self.app.player.start_playing() self.app.player.start_playing()
elif self.app.player.current_playlist is None: elif self.app.player.current_playlist.title == "None":
if self.app.settings.latest_playlist is not None: self.app.player.start_playlist(self.app.gui.clicked_playlist)
for playlist in self.app.library.playlists: # get loaded playlist by the path of the latest playlist
if playlist.path == self.app.settings.latest_playlist:
self.app.player.start_playlist(playlist)
break
def on_playstate_update(self): def on_playstate_update(self):
if self.app.player.playing: if self.app.player.playing:

View file

@ -1,91 +0,0 @@
#!/usr/bin/python3
from PyQt6.QtGui import QPixmap, QFont
from PyQt6.QtWidgets import QToolBar, QWidget, QLabel, QSizePolicy, QVBoxLayout
class TrackInfo(QToolBar):
title_font = QFont()
title_font.setPointSize(16)
title_font.setBold(True)
artist_font = QFont()
title_font.setPointSize(12)
album_font = QFont()
album_font.setPointSize(8)
def __init__(self, app, parent=None):
super().__init__(parent)
self.app = app
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.wobuzz_logo = QPixmap(f"{self.app.utils.wobuzz_location}/icon.svg")
self.track_cover = QLabel(self)
self.track_cover.setFixedSize(64, 64)
self.track_cover.setScaledContents(True)
self.track_cover.setPixmap(self.wobuzz_logo)
self.addWidget(self.track_cover)
self.info_container = QWidget(self)
info_container_layout = QVBoxLayout(self.info_container)
self.info_container.setLayout(info_container_layout)
self.addWidget(self.info_container)
self.title = QLabel("Title", self.info_container)
self.title.setFont(self.title_font)
info_container_layout.addWidget(self.title)
self.artist = QLabel("Artist", self.info_container)
self.artist.setFont(self.artist_font)
info_container_layout.addWidget(self.artist)
self.album = QLabel("Album", self.info_container)
self.album.setFont(self.album_font)
info_container_layout.addWidget(self.album)
def update_info(self):
current_playlist = self.app.player.current_playlist
if current_playlist is not None:
current_track = current_playlist.current_track
title = current_track.tags.title
artist = current_track.tags.artist
album = current_track.tags.album
self.title.setText(title)
if artist is not None and not artist == "":
self.artist.setText(f"By {current_track.tags.artist}")
else:
self.artist.setText("No Artist")
if album is not None and not album == "":
self.album.setText(f"In {current_track.tags.album}")
else:
self.album.setText("No Album")
cover = current_track.tags.images.any
if cover is None:
self.track_cover.setPixmap(self.wobuzz_logo)
return
cover_data = cover.data
if isinstance(cover_data, bytes):
cover_pixmap = QPixmap()
cover_pixmap.loadFromData(cover_data)
self.track_cover.setPixmap(cover_pixmap)
else:
self.track_cover.setPixmap(self.wobuzz_logo)

View file

@ -29,7 +29,7 @@ class TrackProgressSlider(QSlider):
self.sliderPressed.connect(self.on_press) self.sliderPressed.connect(self.on_press)
self.sliderReleased.connect(self.on_release) self.sliderReleased.connect(self.on_release)
def late_init(self): def post_init(self):
self.track_control = self.app.gui.track_control self.track_control = self.app.gui.track_control
def mousePressEvent(self, event: QMouseEvent): def mousePressEvent(self, event: QMouseEvent):
@ -57,7 +57,7 @@ class TrackProgressSlider(QSlider):
def on_release(self): def on_release(self):
self.dragged = False self.dragged = False
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): if self.app.player.current_playlist.has_tracks():
self.app.player.seek(self.value()) self.app.player.seek(self.value())
def update_progress(self): def update_progress(self):