Compare commits

..

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

28 changed files with 137 additions and 613 deletions

1
.gitignore vendored
View file

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

View file

@ -5,50 +5,26 @@ Currently, it just has really basic features but many more things are planned.
### Features
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
## Installation
### Release installation
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:
To install Wobuzz, you firstly have to install the dependencies that can't be installed using pip.
This can be done using:
``` bash
sudo apt install xcb libxcb-cursor0 ffmpeg
sudo apt install pyqt6-dev-tools xcb libxcb-cursor0 ffmpeg
```
Now, you can install the newest unstable version using just one more command:
```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.
Now you can just clone the repo and let pip install it.
``` bash
git clone https://teapot.informationsanarchistik.de/Wobbl/Wobuzz.git
cd Wobuzz
pip install -e .
pip install .
```
## Usage:

View file

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

View file

@ -1,40 +1,29 @@
#!/usr/bin/python3
import setuptools
import os
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
this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()
setuptools.setup(
name="Wobuzz",
version="0.1a2",
version="0.0",
description="An audio player made by The Wobbler",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz",
author="The Wobbler",
author_email="emil@i21k.de",
license="GNU GPLv3",
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
package_data={"": ["*.txt", "*.svg"]},
package_data={"": ["*.txt"]},
install_requires=[
"PyQt6",
"tinytag",
"pydub",
"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={
"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 argparse
from wobuzz.player.playlist import Playlist
def main():
description = "A music player made by The Wobbler."
@ -22,24 +20,23 @@ def main():
app = Wobuzz()
if arguments.playlist:
playlist = Playlist(app, "Temporary Playlist", arguments.playlist)
app.library.playlists.append(playlist)
if app.library.temporary_playlist in app.library.playlists:
app.library.playlists.remove(app.library.temporary_playlist)
app.library.temporary_playlist = playlist
app.library.temporary_playlist.clear()
app.library.temporary_playlist.view.clear()
app.library.temporary_playlist.load_from_m3u(arguments.playlist)
app.library.temporary_playlist.view.load_tracks()
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:
app.library.playlists.remove(app.library.temporary_playlist)
app.library.temporary_playlist = playlist
for track in arguments.track:
tracks.append(os.path.abspath(track))
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())

View file

@ -11,13 +11,13 @@ class GUI:
self.dropped = []
self.clicked_playlist = self.app.library.temporary_playlist
self.window = MainWindow(app)
self.settings = self.window.settings
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(
QDockWidget.DockWidgetFeature.DockWidgetMovable |
@ -52,18 +52,5 @@ class GUI:
def on_track_change(self, previous_track, track):
self.track_control.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()
self.app.player.current_playlist.view.on_track_change(previous_track, track)

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

View file

@ -1,5 +1,6 @@
#!/usr/bin/python3
import os
import sys
from PyQt6.QtWidgets import QApplication
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.set_attribute_change_event(self.on_settings_change)
self.library = Library(self)
self.player = Player(self)
self.library = Library(self)
self.gui = GUI(self)
self.late_init()
self.post_init()
def late_init(self):
self.gui.track_control.track_progress_slider.late_init()
def post_init(self):
self.gui.track_control.track_progress_slider.post_init()
self.library.load()
def on_settings_change(self, key, value):
@ -37,7 +38,4 @@ class Wobuzz:
if __name__ == "__main__":
wobuzz = Wobuzz()
wobuzz.post_init()
wobuzz.library.load_playlist_views()
sys.exit(wobuzz.qt_app.exec())

View file

@ -1,6 +1,5 @@
#!/usr/bin/python3
import time
import threading
import pygame.mixer
import pygame.event
@ -19,7 +18,7 @@ class Player:
self.track_progress = TrackProgress(self.app)
self.history = Playlist(self.app, "History")
self.current_playlist = None
self.current_playlist = Playlist(self.app, "None")
self.playing = False
self.paused = False
@ -33,7 +32,7 @@ class Player:
self.playing = True
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
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_playstate_update()
self.app.gui.track_control.on_playstate_update()
def play_track_in_playlist(self, track_index):
self.stop()
@ -99,7 +98,7 @@ class Player:
self.track_progress.pause()
self.paused = True
self.app.gui.on_playstate_update()
self.app.gui.track_control.on_playstate_update()
def unpause(self):
self.music_channel.unpause()
@ -108,7 +107,7 @@ class Player:
self.playing = True
self.paused = False
self.app.gui.on_playstate_update()
self.app.gui.track_control.on_playstate_update()
def next_track(self):
if not self.current_playlist.on_last_track():
@ -135,13 +134,13 @@ class Player:
self.music_channel.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.playing = False
self.paused = False
self.app.gui.on_playstate_update()
self.app.gui.track_control.on_playstate_update()
def seek(self, position: int):
self.music_channel.stop()
@ -161,15 +160,8 @@ class Player:
track = self.current_playlist.tracks[self.current_playlist.current_track_index + 1]
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()
self.app.gui.on_background_job_stop("Loading Track")
def cache_next_track(self):
# function that creates a thread which will cache the next track
caching_thread = threading.Thread(target=self.caching_thread_function)
@ -178,15 +170,6 @@ class Player:
def start_playlist(self, playlist):
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_playlist = playlist

View file

@ -1,23 +1,15 @@
#!/usr/bin/python3
import os
import threading
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QAbstractItemView
from .track import Track
class Playlist:
def __init__(self, app, title: str, load_from=None):
def __init__(self, app, title: str):
self.app = app
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,
# no other playlist can be created using the same name
self.app.utils.unique_names.append(self.title)
@ -26,12 +18,7 @@ class Playlist:
self.tracks: list[Track] = []
self.current_track_index = 0
self.current_track: Track | None = None
self.views = {} # dict of id(LibraryDock): PlaylistView
self.loaded = False
self.path = os.path.expanduser(
f"{app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
)
self.view = None
def clear(self):
self.sorting: list[Qt.SortOrder] | None = None
@ -40,49 +27,19 @@ class Playlist:
self.current_track = None
def load_from_paths(self, paths):
num_tracks = len(paths)
i = 0
process_title = f'Loading Playlist "{self.title}"'
self.app.gui.on_background_job_start(
process_title,
f'Loading the tracks of "{self.title}".',
num_tracks,
lambda: i
)
while i < num_tracks:
while i < len(paths):
path = paths[i]
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
self.loaded = True
self.app.gui.on_background_job_stop(process_title)
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)
# 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.current_track = self.tracks[0]
def load_from_m3u(self, path):
file = open(path, "r")
@ -92,18 +49,8 @@ class Playlist:
lines = m3u.split("\n") # m3u entries are separated by newlines
lines = lines[:-1] # remove last entry because it is just an empty string
num_lines = len(lines)
i = 0
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
)
num_lines = len(lines)
while i < num_lines:
line = lines[i]
@ -113,7 +60,7 @@ class Playlist:
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
@ -121,12 +68,10 @@ class Playlist:
if self.current_track is None and self.has_tracks():
self.current_track = self.tracks[0]
self.loaded = True
self.app.gui.on_background_job_stop(process_title)
#self.app.player.history.append_track(self.current_track)
def load_from_wbz(self, path):
self.load_from_m3u(path) # placeholder
pass
def has_tracks(self):
return len(self.tracks) > 0
@ -174,44 +119,46 @@ class Playlist:
for track in self.tracks:
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.close()
def rename(self, title: str):
# remove from unique names so a new playlist can have the old name and delete old playlist.
if os.path.exists(self.path):
os.remove(self.path)
path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
path = os.path.expanduser(path)
if os.path.exists(path):
os.remove(os.path.expanduser(path))
old_title = self.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
self.app.utils.unique_names.remove(old_title)
def delete(self):
if os.path.exists(self.path):
os.remove(self.path)
path = f"{self.app.settings.library_path}/playlists/{self.title.replace(" ", "_")}.wbz.m3u"
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.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):
for dock_id in self.views:
view = self.views[dock_id]
view.append_track(track)
self.tracks.append(track)
if self.view:
self.view.append_track(track)
def h_last_track(self):
# get last track in history (only gets used in player.history)

View file

@ -1,20 +1,29 @@
#!/usr/bin/python3
from pydub import AudioSegment
from pydub.effects import normalize
from pygame.mixer import Sound
from tinytag import TinyTag
SUPPORTED_FORMATS = [
"mp3",
"wav",
"ogg"
]
class Track:
"""
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.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.audio = None
@ -33,7 +42,6 @@ class Track:
new_occurrences = {}
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[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 self.app.player.current_playlist is not None:
if self.app.player.current_playlist.current_track is self:
for item in self.items:
if (
item.playlist in self.occurrences and
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)])
if self.app.player.current_playlist.current_track is self:
for item in self.items:
if (
item.playlist in self.occurrences and
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)])
def cache(self):
self.load_audio()
@ -67,8 +74,6 @@ class Track:
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
def clear_cache(self):
@ -78,12 +83,11 @@ class Track:
self.sound = None
self.duration = 0
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False) # metadata without images
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):
remaining_audio = self.audio[position:]

View file

@ -30,5 +30,5 @@ class TrackProgress:
def stop(self):
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

View file

@ -9,6 +9,4 @@ class Settings:
window_maximized: bool=False
library_path: str="~/.wobuzz"
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.setAllowedAreas(
Qt.DockWidgetArea.LeftDockWidgetArea |
Qt.DockWidgetArea.RightDockWidgetArea |
Qt.DockWidgetArea.BottomDockWidgetArea
)
self.setAcceptDrops(True)
self.library_widget = Library(library, self)

View file

@ -1,12 +1,9 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMainWindow, QMenu
from .track_control import TrackControl
from .settings import Settings
from .process.process_dock import ProcessDock
from .track_info import TrackInfo
class MainWindow(QMainWindow):
@ -15,10 +12,7 @@ class MainWindow(QMainWindow):
self.app = app
self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg")
self.setWindowTitle("Wobuzz")
self.setWindowIcon(self.icon)
self.menu_bar = self.menuBar()
@ -30,11 +24,6 @@ class MainWindow(QMainWindow):
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.addToolBar(self.track_control)
@ -42,13 +31,5 @@ class MainWindow(QMainWindow):
self.settings.hide()
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.processes_action.triggered.connect(self.process_dock.show)

View file

@ -10,20 +10,19 @@ from .track import TrackItem
class PlaylistView(QTreeWidget):
itemDropped = pyqtSignal(QTreeWidget, list)
normal_font = QFont()
bold_font = QFont()
bold_font.setBold(True)
def __init__(self, playlist, dock, parent=None):
def __init__(self, playlist, parent=None):
super().__init__(parent)
self.playlist = playlist
self.library_dock = dock
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.setColumnCount(4)
@ -40,6 +39,8 @@ class PlaylistView(QTreeWidget):
self.setHeaderLabels(headers)
self.load_tracks()
self.itemActivated.connect(self.on_track_activation)
def on_user_sort(self):
@ -62,8 +63,7 @@ class PlaylistView(QTreeWidget):
i += 1
# make sure the next track is cached (could be moved by user)
if self.app.player.current_playlist == self.playlist and self.app.player.current_playlist.has_tracks():
if self.app.player.current_playlist.has_tracks():
self.app.player.cache_next_track()
def dropEvent(self, event: QDropEvent):

View file

@ -36,10 +36,6 @@ class PlaylistTabBar(QTabBar):
def on_doubleclick(self, 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
self.app.player.start_playlist(playlist)

View file

@ -1,7 +1,7 @@
#!/usr/bin/python3
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QMouseEvent, QCursor
from PyQt6.QtGui import QMouseEvent
from PyQt6.QtWidgets import QLineEdit
from .tab_bar import PlaylistTabBar
@ -16,10 +16,9 @@ class TabTitle(QLineEdit):
self.index = index
self.playlist_view = playlist_view
self.setStyleSheet("QLineEdit {background: transparent; border: none;}")
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
self.setStyleSheet("QLineEdit {background: transparent;}")
self.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) # normal cursor (would be a text cursor)
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
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.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.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
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.load_on_start.setChecked(self.app.settings.load_on_start)
def update_settings(self, key, value):
match key:
@ -54,11 +53,7 @@ class Settings(QDockWidget):
case "clear_track_cache":
self.behavior_settings.clear_track_cache.setDown(value)
case "load_on_start":
self.behavior_settings.load_on_start.setChecked(value)
def write_settings(self):
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.load_on_start = self.behavior_settings.load_on_start.isChecked()

View file

@ -46,15 +46,15 @@ class TrackControl(QToolBar):
self.next_button.triggered.connect(self.next_track)
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()
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()
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()
def on_track_change(self, previous_track, track):
@ -80,17 +80,11 @@ class TrackControl(QToolBar):
elif self.app.player.playing: # playing
self.app.player.pause()
# stopped but tracks in the current playlist
elif self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks():
elif self.app.player.current_playlist.has_tracks(): # stopped but tracks in the current playlist
self.app.player.start_playing()
elif self.app.player.current_playlist is None:
if self.app.settings.latest_playlist is not None:
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
elif self.app.player.current_playlist.title == "None":
self.app.player.start_playlist(self.app.gui.clicked_playlist)
def on_playstate_update(self):
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.sliderReleased.connect(self.on_release)
def late_init(self):
def post_init(self):
self.track_control = self.app.gui.track_control
def mousePressEvent(self, event: QMouseEvent):
@ -57,7 +57,7 @@ class TrackProgressSlider(QSlider):
def on_release(self):
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())
def update_progress(self):