Compare commits
No commits in common. "main" and "main" have entirely different histories.
51 changed files with 437 additions and 1972 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
|||
wobuzz/settings.json
|
||||
Wobuzz.egg-info
|
||||
build
|
||||
__pycache__
|
||||
.idea
|
64
README.md
64
README.md
|
@ -2,75 +2,29 @@
|
|||
|
||||
Wobuzz is a simple audio player made by The Wobbler.
|
||||
Currently, it just has really basic features but many more things are planned.
|
||||
The player has its own playlist file format that is similar to extended m3u. [WOBUZZM3U](https://gulm.i21k.de/index.php?title=WOBUZZM3U)
|
||||
|
||||

|
||||
|
||||
Please note that [the repository on teapot.informationsanarchistik.de](https://teapot.informationsanarchistik.de/Wobbl/Wobuzz) is the original repository and the [repository on Codeberg](https://codeberg.org/Wobbl/Wobuzz) is a mirror,
|
||||
normal users only have read rights on the original repository because registration is disabled on the server.
|
||||
Issues and pull-requests are not synced.
|
||||
|
||||
### 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 |
|
||||
|
||||
### Performance
|
||||
|
||||
Currently, Wobuzz is relatively CPU-friendly in comparison to other audio players, but not RAM-friendly.
|
||||
In comparison to Audacious, Wobuzz uses half as much CPU, but more than double the RAM.
|
||||
This is because Audacious loads the audio only partially, while Wobuzz loads the entire track and the following one.
|
||||
In the future, this may get optimized and CPU-usage could increase due to more 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 |
|
||||
| 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
|
||||
|
||||
#### Compatibility
|
||||
|
||||
Wobuzz was only made for Linux.
|
||||
It should not require that much effort to make it compatible with windows or mac, but why should anyone do that???
|
||||
Currently (v0.1a2) Wobuzz is not really tested for compatibility, but it should run without problems on rather
|
||||
new Debian-based distros like Mint 22. \
|
||||
Also, the Python version should be newer than Python3.9
|
||||
|
||||
### 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 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:
|
||||
|
||||
```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:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
PyQt6
|
||||
pygame
|
||||
tinytag
|
||||
pydub
|
||||
wobbl_tools @ git+https://teapot.informationsanarchistik.de/Wobbl/wobbl_tools@9b7e796877781f77f6df93475750c15a0ca51dd9#egg=wobbl_tools
|
||||
pydub
|
19
setup.py
19
setup.py
|
@ -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"],
|
||||
|
|
|
@ -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;
|
|
@ -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,14 +20,23 @@ def main():
|
|||
app = Wobuzz()
|
||||
|
||||
if arguments.playlist:
|
||||
playlist = Playlist(app, "Temporary Playlist", arguments.playlist)
|
||||
|
||||
app.library.replace_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:
|
||||
app.library.open_tracks(arguments.track)
|
||||
app.library.temporary_playlist.clear()
|
||||
app.library.temporary_playlist.view.clear()
|
||||
|
||||
app.library.load_playlist_views()
|
||||
# make track paths absolute
|
||||
tracks = []
|
||||
|
||||
for track in arguments.track:
|
||||
tracks.append(os.path.abspath(track))
|
||||
|
||||
app.library.temporary_playlist.load_from_paths(tracks)
|
||||
app.library.temporary_playlist.view.load_tracks()
|
||||
|
||||
sys.exit(app.qt_app.exec())
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtWidgets import QDockWidget, QFileDialog
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QDockWidget
|
||||
from .ui.main_window import MainWindow
|
||||
from .ui.popups import Popups
|
||||
|
||||
|
||||
class GUI:
|
||||
|
@ -13,36 +11,35 @@ class GUI:
|
|||
|
||||
self.dropped = []
|
||||
|
||||
self.window = MainWindow(app, self)
|
||||
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.popups = Popups(app, self)
|
||||
self.window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.app.library.main_library_dock)
|
||||
|
||||
self.window.setCentralWidget(self.app.library.main_library_widget)
|
||||
self.app.library.main_library_dock.setFeatures(
|
||||
QDockWidget.DockWidgetFeature.DockWidgetMovable |
|
||||
QDockWidget.DockWidgetFeature.DockWidgetFloatable
|
||||
)
|
||||
|
||||
if self.app.settings.window_maximized:
|
||||
self.window.showMaximized()
|
||||
|
||||
elif self.app.settings.window_size is not None:
|
||||
elif not self.app.settings.window_size is None:
|
||||
self.window.resize(*self.app.settings.window_size)
|
||||
|
||||
self.gui_update_timer = QTimer()
|
||||
self.gui_update_timer.timeout.connect(self.update_gui)
|
||||
self.gui_update_timer.start(1000 // self.app.settings.gui_update_rate)
|
||||
|
||||
self.window.closeEvent = self.on_exit
|
||||
self.connect()
|
||||
|
||||
self.window.show()
|
||||
|
||||
self.settings.update_all()
|
||||
|
||||
def on_exit(self, event):
|
||||
self.window.focusWidget().clearFocus() # clear focus on focused widget
|
||||
def connect(self):
|
||||
self.window.closeEvent = self.on_exit
|
||||
|
||||
self.app.player.stop()
|
||||
def on_exit(self, event):
|
||||
self.app.library.on_exit(event)
|
||||
|
||||
self.app.settings.window_size = (self.window.width(), self.window.height())
|
||||
|
@ -53,31 +50,7 @@ class GUI:
|
|||
def on_settings_change(self, key, value):
|
||||
self.settings.update_settings(key, value)
|
||||
|
||||
match key:
|
||||
case "gui_update_rate":
|
||||
self.gui_update_timer.setInterval(1000 // value)
|
||||
|
||||
case "album_cover_size":
|
||||
self.track_info.set_size(value)
|
||||
|
||||
def on_track_change(self, 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 library_widget_id in self.app.player.current_playlist.views:
|
||||
view = self.app.player.current_playlist.views[library_widget_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()
|
||||
|
||||
def update_gui(self):
|
||||
self.track_control.track_progress_slider.update_progress()
|
||||
if self.process_dock.isVisible():
|
||||
self.process_dock.update_processes()
|
||||
|
|
|
@ -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 |
|
@ -1,8 +1 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
def __getattr__(name):
|
||||
match name:
|
||||
case "Library":
|
||||
from .library import Library
|
||||
|
||||
return Library
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
from PyQt6.QtWidgets import QTabWidget, QAbstractItemView
|
||||
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
from ..player.playlist import Playlist
|
||||
from ..ui.library.library import LibraryWidget
|
||||
from ..ui.playlist_view import PlaylistView
|
||||
from ..types import Types
|
||||
from ..ui.library_dock import LibraryDock
|
||||
from ..ui.playlist import PlaylistView
|
||||
|
||||
|
||||
class Library:
|
||||
|
@ -17,20 +15,13 @@ class Library:
|
|||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
self.main_library_widget = LibraryWidget(self)
|
||||
self.library_widgets = [self.main_library_widget]
|
||||
self.main_library_dock = LibraryDock(self)
|
||||
self.library_docks = [self.main_library_dock]
|
||||
|
||||
self.loaded_tracks = {} # dict of {track path: track}
|
||||
|
||||
self.playlists = []
|
||||
self.temporary_playlist = None
|
||||
|
||||
self.artist_playlists = []
|
||||
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):
|
||||
|
@ -46,137 +37,38 @@ 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):
|
||||
# create views for each dock and playlist
|
||||
for library_dock in self.library_docks:
|
||||
playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs
|
||||
|
||||
for library_widget in self.library_widgets:
|
||||
playlist_tabs: QTabWidget = library_widget.playlist_tabs
|
||||
playlist_tabs.playlists = {}
|
||||
|
||||
# create view for each playlist
|
||||
for playlist in self.playlists:
|
||||
if id(library_widget) in playlist.views: # view already exists
|
||||
continue
|
||||
|
||||
playlist_view = PlaylistView(playlist, library_widget)
|
||||
playlist_view = PlaylistView(playlist)
|
||||
playlist_tabs.addTab(playlist_view, playlist.title)
|
||||
|
||||
if playlist.path == self.app.settings.latest_playlist: # start with latest playlist opened and loaded
|
||||
playlist_tabs.setCurrentIndex(playlist_tabs.count() - 1)
|
||||
|
||||
playlist.load()
|
||||
|
||||
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_widget in self.library_widgets:
|
||||
playlist_tabs: QTabWidget = library_widget.playlist_tabs
|
||||
|
||||
playlist_view = PlaylistView(playlist, library_widget, playlist_tabs)
|
||||
playlist_view.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # enable drag n drop
|
||||
for library_dock in self.library_docks:
|
||||
playlist_tabs: QTabWidget = library_dock.library_widget.playlist_tabs
|
||||
|
||||
playlist_view = PlaylistView(playlist)
|
||||
playlist_tabs.addTab(playlist_view, playlist.title)
|
||||
|
||||
def replace_temporary_playlist(self, replace: Playlist):
|
||||
if self.temporary_playlist is not None:
|
||||
self.temporary_playlist.delete()
|
||||
|
||||
if not replace in self.playlists:
|
||||
self.playlists.append(replace)
|
||||
|
||||
self.temporary_playlist = replace
|
||||
|
||||
def open_tracks(self, tracks: list[str]):
|
||||
playlist = Playlist(self.app, "Temporary Playlist", tracks)
|
||||
|
||||
self.replace_temporary_playlist(playlist)
|
||||
|
||||
self.load_playlist_views()
|
||||
|
||||
playlist.load()
|
||||
|
||||
def import_tracks(
|
||||
self,
|
||||
tracks: list[str],
|
||||
import_options: Types.ImportOptions
|
||||
):
|
||||
playlist = Playlist(self.app, "Temporary Playlist", tracks, import_options)
|
||||
|
||||
self.replace_temporary_playlist(playlist)
|
||||
|
||||
self.load_playlist_views()
|
||||
playlist.load()
|
||||
|
||||
def import_track(self, track, import_options: Types.ImportOptions):
|
||||
change_metadata = False
|
||||
|
||||
if import_options.artist is not None:
|
||||
track.metadata.artist = import_options.artist
|
||||
change_metadata = True
|
||||
|
||||
if import_options.album is not None:
|
||||
track.metadata.album = import_options.album
|
||||
change_metadata = True
|
||||
|
||||
if import_options.genre is not None:
|
||||
track.metadata.genre = import_options.genre
|
||||
change_metadata = True
|
||||
|
||||
artist_path = os.path.expanduser(f"{self.app.settings.library_path}/artists/{track.metadata.artist}")
|
||||
|
||||
if not os.path.exists(artist_path):
|
||||
os.makedirs(artist_path)
|
||||
|
||||
new_track_path = f"{artist_path}/{track.path.split('/')[-1]}"
|
||||
|
||||
if track.path == new_track_path or os.path.exists(new_track_path): # track is already in the library
|
||||
return
|
||||
|
||||
track.copy(new_track_path, import_options.copy_type)
|
||||
|
||||
def open_playlist(self, playlist_path: str):
|
||||
playlist = Playlist(self.app, "Temporary Playlist", playlist_path)
|
||||
|
||||
self.replace_temporary_playlist(playlist)
|
||||
|
||||
self.load_playlist_views()
|
||||
|
||||
playlist.load()
|
||||
|
||||
def import_playlist(self, playlist_path: str, import_options):
|
||||
playlist = Playlist(self.app, "Temporary Playlist", playlist_path, import_options)
|
||||
|
||||
self.replace_temporary_playlist(playlist)
|
||||
|
||||
self.load_playlist_views()
|
||||
|
||||
playlist.load()
|
||||
|
||||
def loaded_track(self, track_path: str):
|
||||
"""
|
||||
Returns either a loaded track with the given path or None if there is none.
|
||||
"""
|
||||
|
||||
if track_path in self.loaded_tracks:
|
||||
return self.loaded_tracks[track_path]
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
@ -89,7 +88,7 @@ class Player:
|
|||
|
||||
if (
|
||||
self.app.settings.clear_track_cache and
|
||||
last_track is not None and
|
||||
not last_track is None and
|
||||
not last_track == self.current_playlist.current_track
|
||||
):
|
||||
last_track.clear_cache()
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -1,47 +1,24 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import threading
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QAbstractItemView
|
||||
|
||||
from .track import Track, TrackMetadata
|
||||
from ..wobuzzm3u import WobuzzM3U, WBZM3UData
|
||||
from ..types import Types
|
||||
from .track import Track
|
||||
|
||||
|
||||
class Playlist:
|
||||
def __init__(self, app, title: str, load_from=None, import_options: Types.ImportOptions=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
|
||||
|
||||
self.import_options = import_options
|
||||
|
||||
# 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)
|
||||
|
||||
# sort order
|
||||
self.sorting: list[WBZM3UData.SortOrder] = [
|
||||
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_title, True),
|
||||
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_artist, True),
|
||||
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_album, True),
|
||||
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_genre, True),
|
||||
WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True)
|
||||
]
|
||||
self.sorting: list[Qt.SortOrder] | None = None # Custom sort order if None
|
||||
self.tracks: list[Track] = []
|
||||
self.current_track_index = 0
|
||||
self.current_track: Track | None = None
|
||||
self.views = {} # dict of id(LibraryWidget): PlaylistView
|
||||
self.loaded = False
|
||||
self.loading = False
|
||||
|
||||
self.path = self.path_from_title(title)
|
||||
self.view = None
|
||||
|
||||
def clear(self):
|
||||
self.sorting: list[Qt.SortOrder] | None = None
|
||||
|
@ -50,60 +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.loaded or self.loading:
|
||||
return
|
||||
|
||||
self.loading = True
|
||||
|
||||
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)
|
||||
|
||||
self.loading = False
|
||||
|
||||
if self.import_options is not None:
|
||||
for track in self.tracks:
|
||||
self.app.library.import_track(track, self.import_options)
|
||||
|
||||
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")
|
||||
|
@ -113,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]
|
||||
|
@ -134,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
|
||||
|
||||
|
@ -142,110 +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):
|
||||
file = open(path, "r")
|
||||
m3u = file.read()
|
||||
file.close()
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
wbzm3u = WobuzzM3U(self.path)
|
||||
track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U
|
||||
|
||||
while i < num_lines:
|
||||
line = lines[i]
|
||||
|
||||
line_data = wbzm3u.parse_line(line)
|
||||
|
||||
if line_data is None:
|
||||
i += 1
|
||||
|
||||
continue
|
||||
|
||||
if line_data.is_comment: # comments and EXTM3U/WOBUZZM3U
|
||||
if isinstance(line_data, WBZM3UData.SortOrder): # sort
|
||||
del self.sorting[0] # delete first sort so the length stays at 6
|
||||
|
||||
self.sorting.append(line_data)
|
||||
|
||||
if isinstance(line_data, WBZM3UData.TrackMetadata.TrackTitle):
|
||||
track_metadata.title = line_data
|
||||
|
||||
if isinstance(line_data, WBZM3UData.TrackMetadata.TrackArtist):
|
||||
track_metadata.artist = line_data
|
||||
|
||||
if isinstance(line_data, WBZM3UData.TrackMetadata.TrackAlbum):
|
||||
track_metadata.album = line_data
|
||||
|
||||
i += 1
|
||||
|
||||
continue
|
||||
|
||||
elif isinstance(line_data, WBZM3UData.URL): # ignore urls
|
||||
i += 1
|
||||
|
||||
continue
|
||||
|
||||
track_metadata.path = line
|
||||
track_metadata.add_missing()
|
||||
|
||||
self.append_track(Track(self.app, line, cache=i == 0, metadata=track_metadata)) # first track is cached
|
||||
|
||||
track_metadata = TrackMetadata() # metadata for next track
|
||||
|
||||
i += 1
|
||||
|
||||
# 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]
|
||||
|
||||
list(self.views.values())[0].sort() # execute sort() on the first view
|
||||
|
||||
self.loaded = True
|
||||
|
||||
self.app.gui.on_background_job_stop(process_title)
|
||||
|
||||
def sync(self, view, user_sort: bool=False):
|
||||
num_tracks = view.topLevelItemCount()
|
||||
|
||||
i = 0
|
||||
|
||||
while i < num_tracks:
|
||||
track_item = view.topLevelItem(i)
|
||||
track = track_item.track
|
||||
|
||||
track_item.index = i
|
||||
|
||||
if user_sort:
|
||||
track_item.index_user_sort = i
|
||||
|
||||
self.tracks[i] = track
|
||||
|
||||
track.set_occurrences()
|
||||
|
||||
i += 1
|
||||
|
||||
# make sure the next track is cached (could be moved by user)
|
||||
if self.app.player.current_playlist == self and self.has_tracks():
|
||||
self.app.player.cache_next_track()
|
||||
pass
|
||||
|
||||
def has_tracks(self):
|
||||
return len(self.tracks) > 0
|
||||
|
@ -288,86 +114,54 @@ class Playlist:
|
|||
return self.current_track.sound, self.current_track.duration
|
||||
|
||||
def save(self):
|
||||
first_view = list(self.views.values())[0]
|
||||
first_view.sortItems(5, Qt.SortOrder.AscendingOrder) # sort by custom sorting
|
||||
self.sync(first_view)
|
||||
|
||||
wbzm3u = WobuzzM3U(self.path)
|
||||
|
||||
wbz_data = ""
|
||||
|
||||
wbz_data += wbzm3u.assemble_line(WBZM3UData.Header)
|
||||
|
||||
for order in self.sorting:
|
||||
wbz_data += wbzm3u.assemble_line(order)
|
||||
|
||||
for track in self.tracks:
|
||||
# cache track metadata
|
||||
wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackTitle(track.metadata.title))
|
||||
wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackArtist(track.metadata.artist))
|
||||
wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackAlbum(track.metadata.album))
|
||||
wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackGenre(track.metadata.genre))
|
||||
wbz_data += f"{track.path}\n"
|
||||
|
||||
wbz_data += wbzm3u.assemble_line(WBZM3UData.Path(track.path))
|
||||
|
||||
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):
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
# remove from unique names so a new playlist can have the old name and delete old playlist.
|
||||
|
||||
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 = self.path_from_title(self.title)
|
||||
|
||||
# make sure the playlist is not referenced anymore as the temporary playlist
|
||||
if self == self.app.library.temporary_playlist:
|
||||
self.app.library.temporary_playlist = None
|
||||
|
||||
# remove from unique names so a new playlist can have the old name and delete old playlist.
|
||||
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 self.app.player.current_playlist == self: # stop if this is the current playlist
|
||||
self.app.player.stop()
|
||||
self.app.player.current_playlist = None
|
||||
|
||||
for view in self.views.values(): # close views (and PyQt automatically closes the corresponding tabs)
|
||||
view.deleteLater()
|
||||
|
||||
for track in self.tracks: # remove items that corresponded to the track and this playlist
|
||||
track.delete_items(self)
|
||||
|
||||
# make sure the playlist is not referenced as the temporary playlist
|
||||
if self is self.app.library.temporary_playlist:
|
||||
self.app.library.temporary_playlist = None
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
if len(self.tracks) > 1:
|
||||
return self.tracks[-2]
|
||||
|
||||
def path_from_title(self, title):
|
||||
path = os.path.expanduser(
|
||||
f"{self.app.settings.library_path}/playlists/{title.replace(' ', '_')}.wbz.m3u"
|
||||
)
|
||||
|
||||
return path
|
||||
|
|
|
@ -1,71 +1,29 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pydub import AudioSegment
|
||||
from pydub.effects import normalize
|
||||
from pygame.mixer import Sound
|
||||
from tinytag import TinyTag
|
||||
from tinytag.tinytag import Images as TTImages
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..types import Types
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackMetadata:
|
||||
path: str | None=None
|
||||
title: str | None=None
|
||||
artist: str | None=None
|
||||
album: str | None=None
|
||||
genre: str | None=None
|
||||
images: TTImages | None=None # tinytag images
|
||||
|
||||
def add_missing(self):
|
||||
# Make the album be an empty string instead of "None"
|
||||
if self.title == "None":
|
||||
self.title = ""
|
||||
|
||||
if self.artist == "None":
|
||||
self.artist = ""
|
||||
|
||||
if self.album == "None":
|
||||
self.album = ""
|
||||
|
||||
if self.genre == "None":
|
||||
self.genre = ""
|
||||
|
||||
if self.path is None: # can't add missing information without a path
|
||||
return
|
||||
|
||||
if self.title is None or self.artist is None or self.album is None or self.genre is None:
|
||||
tags = TinyTag.get(self.path, ignore_errors=True, duration=False)
|
||||
|
||||
self.title = tags.title
|
||||
self.artist = tags.artist
|
||||
self.album = tags.album
|
||||
self.genre = tags.genre
|
||||
SUPPORTED_FORMATS = [
|
||||
"mp3",
|
||||
"wav",
|
||||
"ogg"
|
||||
]
|
||||
|
||||
|
||||
class Track:
|
||||
"""
|
||||
Class representing a track.
|
||||
Class containing data for a track like file path, raw data...
|
||||
"""
|
||||
|
||||
def __init__(self, app, path: str, cache: bool=False, metadata: TrackMetadata=None):
|
||||
def __init__(self, app, path: str, property_string: str=None, cache: bool=False):
|
||||
self.app = app
|
||||
self.path = path
|
||||
self.property_string = property_string
|
||||
|
||||
# add self to loaded tracks to make sure that no other track object is created for this track
|
||||
app.library.loaded_tracks[self.path] = self
|
||||
|
||||
if metadata is None:
|
||||
# load metadata from audio file
|
||||
tags = TinyTag.get(path, ignore_errors=True, duration=False)
|
||||
|
||||
self.metadata = TrackMetadata(path, tags.title, tags.artist, tags.album)
|
||||
|
||||
else:
|
||||
self.metadata = metadata
|
||||
self.tags = TinyTag.get(self.path, ignore_errors=False, duration=False)
|
||||
|
||||
self.cached = False
|
||||
self.audio = None
|
||||
|
@ -73,30 +31,17 @@ class Track:
|
|||
self.duration = 0
|
||||
|
||||
self.items = []
|
||||
self.occurrences = {} # all occurrences in playlists categorized by playlist and id of the track widget
|
||||
self.occurrences = {} # all occurrences in playlists categorized by playlist and track widget
|
||||
|
||||
if cache:
|
||||
self.cache()
|
||||
|
||||
def __new__(cls, app, path: str, cache: bool=False, metadata: TrackMetadata=None):
|
||||
loaded_track = app.library.loaded_track(path)
|
||||
|
||||
if loaded_track is not None:
|
||||
if cache:
|
||||
loaded_track.cache()
|
||||
|
||||
return loaded_track
|
||||
|
||||
else:
|
||||
return super().__new__(cls)
|
||||
|
||||
def set_occurrences(self):
|
||||
# set track item for every occurrence of track in a playlist
|
||||
|
||||
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
|
||||
|
||||
|
@ -111,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()
|
||||
|
@ -130,9 +74,6 @@ class Track:
|
|||
|
||||
self.duration = len(self.audio) # track duration in milliseconds
|
||||
|
||||
# metadata with images
|
||||
self.metadata.images = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True).images
|
||||
|
||||
self.cached = True
|
||||
|
||||
def clear_cache(self):
|
||||
|
@ -142,10 +83,11 @@ class Track:
|
|||
self.sound = None
|
||||
self.duration = 0
|
||||
|
||||
self.metadata.images = None
|
||||
|
||||
def load_audio(self):
|
||||
self.audio = AudioSegment.from_file(self.path)
|
||||
file_type = self.path.split(".")[-1]
|
||||
|
||||
if file_type in SUPPORTED_FORMATS:
|
||||
self.audio = AudioSegment.from_file(self.path)
|
||||
|
||||
def remaining(self, position: int):
|
||||
remaining_audio = self.audio[position:]
|
||||
|
@ -156,29 +98,3 @@ class Track:
|
|||
|
||||
# return the remaining part of the track's audio and the duration of the remaining part
|
||||
return sound, len(remaining_audio)
|
||||
|
||||
def delete_items(self, playlist):
|
||||
"""
|
||||
Deletes all QTreeWidgetItems that correspond to this track and the given playlist.
|
||||
"""
|
||||
|
||||
for item in self.items:
|
||||
if id(item) in self.occurrences[playlist]:
|
||||
self.items.remove(item)
|
||||
|
||||
self.occurrences.pop(playlist)
|
||||
|
||||
def copy(self, dest: str, copy_type: int=Types.CopyType.symlink, moved: bool=True):
|
||||
match copy_type:
|
||||
case Types.CopyType.symlink:
|
||||
os.symlink(self.path, dest)
|
||||
|
||||
case Types.CopyType.copy:
|
||||
shutil.copyfile(self.path, dest)
|
||||
|
||||
case Types.CopyType.move:
|
||||
shutil.move(self.path, dest)
|
||||
|
||||
if moved: # update path variables
|
||||
self.path = dest
|
||||
self.metadata.path = dest
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,8 +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=False
|
||||
gui_update_rate: int=20
|
||||
album_cover_size: int=64
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
def __getattr__(name):
|
||||
match name:
|
||||
case "Types":
|
||||
from .types import Types
|
||||
return Types
|
||||
|
||||
case "ImportOptions":
|
||||
from .import_options import ImportOptions
|
||||
return ImportOptions
|
||||
|
||||
case "CopyType":
|
||||
from .import_options import CopyType
|
||||
return CopyType
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportOptions:
|
||||
artist: str=None
|
||||
album: str=None
|
||||
genre: str=None
|
||||
|
||||
copy_type=0
|
||||
|
||||
|
||||
class CopyType:
|
||||
symlink = 0
|
||||
copy = 1
|
||||
move = 2
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from . import ImportOptions
|
||||
from . import CopyType
|
||||
|
||||
|
||||
class Types:
|
||||
ImportOptions = ImportOptions
|
||||
CopyType = CopyType
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
match name:
|
||||
case "GroupBox":
|
||||
from .group_box import GroupBox
|
||||
return GroupBox
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QGroupBox, QSizePolicy
|
||||
|
||||
|
||||
class GroupBox(QGroupBox):
|
||||
"""
|
||||
Just a QGroupBox with some custom style I don't always want to rewrite.
|
||||
"""
|
||||
|
||||
def __init__(self, title, parent=None):
|
||||
super().__init__(title, parent)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
self.setStyleSheet("QGroupBox{font-weight: bold;}")
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtWidgets import QToolBox, QLabel, QToolButton
|
||||
from wobuzz.ui.playlist_tabs import PlaylistTabs
|
||||
from PyQt6.QtWidgets import QToolBox, QLabel, QTabWidget, QToolButton
|
||||
from .playlist_tabs import PlaylistTabs
|
||||
|
||||
|
||||
class LibraryWidget(QToolBox):
|
||||
class Library(QToolBox):
|
||||
def __init__(self, library, parent=None):
|
||||
super().__init__(parent)
|
||||
|
|
@ -1 +0,0 @@
|
|||
#!/usr/bin/python3
|
|
@ -1,42 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
|
||||
|
||||
from ..playlist_view import PlaylistView
|
||||
|
||||
|
||||
class ArtistView(PlaylistView):
|
||||
def __init__(self, playlist, library_widget, parent=None):
|
||||
QTreeWidget.__init__(self, parent)
|
||||
|
||||
self.playlist = playlist
|
||||
self.library_widget = library_widget
|
||||
|
||||
self.app = playlist.app
|
||||
|
||||
self.header = self.header()
|
||||
self.header.setSectionsClickable(True)
|
||||
self.header.setSortIndicatorShown(True)
|
||||
|
||||
playlist.views[id(self.library_widget)] = self # let the playlist know that this view exists
|
||||
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
self.setColumnCount(3)
|
||||
|
||||
headers = [
|
||||
"#",
|
||||
"Title",
|
||||
"Artist",
|
||||
"Album",
|
||||
]
|
||||
|
||||
self.setHeaderLabels(headers)
|
||||
|
||||
self.itemActivated.connect(self.on_track_activation)
|
||||
self.header.sectionClicked.connect(self.on_header_click)
|
||||
self.sort_signal.connect(self.sortItems)
|
||||
|
||||
def setDragDropMode(self, behavior):
|
||||
pass # user should not be able to sort the playlist manually
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget,
|
||||
QLabel,
|
||||
QDialog,
|
||||
QCheckBox,
|
||||
QLineEdit,
|
||||
QDialogButtonBox,
|
||||
QButtonGroup,
|
||||
QRadioButton,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QFormLayout,
|
||||
QSizePolicy,
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from ..custom_widgets import GroupBox
|
||||
|
||||
|
||||
class ImportDialog(QDialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Import")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.tagging_section = GroupBox("Metadata And Tagging", self)
|
||||
self.tagging_section.layout = QFormLayout(self.tagging_section)
|
||||
self.tagging_section.setLayout(self.tagging_section.layout)
|
||||
layout.addWidget(self.tagging_section)
|
||||
|
||||
self.tagging_section.overwrite_metadata = QCheckBox(
|
||||
"Set custom metadata for all tracks (Leave property blank to keep the metadata from the audio file.)",
|
||||
self.tagging_section
|
||||
)
|
||||
self.tagging_section.layout.addRow(self.tagging_section.overwrite_metadata)
|
||||
self.tagging_section.overwrite_metadata.setEnabled(False) # writing of metadata not yet implemented
|
||||
|
||||
self.tagging_section.artist = QLineEdit(self.tagging_section)
|
||||
self.tagging_section.layout.addRow(" Artist: ", self.tagging_section.artist)
|
||||
self.tagging_section.artist.setPlaceholderText("Keep track artist")
|
||||
|
||||
self.tagging_section.album = QLineEdit(self.tagging_section)
|
||||
self.tagging_section.layout.addRow(" Album: ", self.tagging_section.album)
|
||||
self.tagging_section.album.setPlaceholderText("Keep track album")
|
||||
|
||||
self.tagging_section.genre = QLineEdit(self.tagging_section)
|
||||
self.tagging_section.layout.addRow(" Genre: ", self.tagging_section.genre)
|
||||
self.tagging_section.genre.setPlaceholderText("Keep track genre")
|
||||
|
||||
self.file_section = GroupBox("File Structure", self)
|
||||
self.file_section.layout = QFormLayout(self.file_section)
|
||||
self.file_section.setLayout(self.file_section.layout)
|
||||
layout.addWidget(self.file_section)
|
||||
|
||||
self.file_section.copy_type_description = QLabel("How should the tracks get put into the Wobuzz library?")
|
||||
self.file_section.layout.addRow(self.file_section.copy_type_description)
|
||||
|
||||
self.file_section.copy_type = QButtonGroup(self.file_section)
|
||||
|
||||
self.file_section.copy_type_symlink = QRadioButton("Create symlinks", self.file_section)
|
||||
self.file_section.copy_type_symlink.setChecked(True)
|
||||
self.file_section.copy_type.addButton(self.file_section.copy_type_symlink)
|
||||
self.file_section.layout.addWidget(self.file_section.copy_type_symlink)
|
||||
|
||||
self.file_section.copy_type_copy = QRadioButton("Copy tracks", self.file_section)
|
||||
self.file_section.copy_type.addButton(self.file_section.copy_type_copy)
|
||||
self.file_section.layout.addWidget(self.file_section.copy_type_copy)
|
||||
|
||||
self.file_section.copy_type_move = QRadioButton("Move tracks", self.file_section)
|
||||
self.file_section.copy_type.addButton(self.file_section.copy_type_move)
|
||||
self.file_section.layout.addWidget(self.file_section.copy_type_move)
|
||||
|
||||
# add expanding widget so the GroupBoxes aren't vertically centered
|
||||
spacer_widget = QWidget(self)
|
||||
spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
layout.addWidget(spacer_widget)
|
||||
|
||||
dialog_buttons = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
|
||||
self.dialog_buttons = QDialogButtonBox(dialog_buttons)
|
||||
layout.addWidget(self.dialog_buttons)
|
||||
|
||||
self.reset_inputs()
|
||||
|
||||
self.tagging_section.overwrite_metadata.stateChanged.connect(self.overwrite_state_changed)
|
||||
self.dialog_buttons.accepted.connect(self.accept)
|
||||
self.dialog_buttons.rejected.connect(self.reject)
|
||||
|
||||
def exec(self):
|
||||
self.reset_inputs()
|
||||
|
||||
return super().exec()
|
||||
|
||||
def overwrite_state_changed(self, state: int):
|
||||
overwrite = state == 2
|
||||
|
||||
self.tagging_section.artist.setEnabled(overwrite)
|
||||
self.tagging_section.album.setEnabled(overwrite)
|
||||
self.tagging_section.genre.setEnabled(overwrite)
|
||||
|
||||
self.tagging_section.layout.setRowVisible(self.tagging_section.artist, overwrite)
|
||||
self.tagging_section.layout.setRowVisible(self.tagging_section.album, overwrite)
|
||||
self.tagging_section.layout.setRowVisible(self.tagging_section.genre, overwrite)
|
||||
|
||||
def reset_inputs(self):
|
||||
self.overwrite_state_changed(0)
|
||||
|
||||
self.tagging_section.artist.setText("")
|
||||
self.tagging_section.album.setText("")
|
||||
self.tagging_section.genre.setText("")
|
||||
|
||||
self.file_section.copy_type_symlink.setChecked(True)
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import QDockWidget
|
||||
from wobuzz.ui.library import LibraryWidget
|
||||
from .library import Library
|
||||
|
||||
|
||||
class LibraryDock(QDockWidget):
|
||||
|
@ -10,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)
|
|
@ -1,45 +1,28 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QIcon, QShortcut
|
||||
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):
|
||||
def __init__(self, app, gui, parent=None):
|
||||
def __init__(self, app, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.app = app
|
||||
self.gui = gui
|
||||
|
||||
self.icon = QIcon(f"{self.app.utils.wobuzz_location}/icon.svg")
|
||||
|
||||
self.setWindowTitle("Wobuzz")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
self.menu_bar = self.menuBar()
|
||||
|
||||
self.file_menu = QMenu("&File", self.menu_bar)
|
||||
self.menu_bar.addMenu(self.file_menu)
|
||||
|
||||
self.open_track_action = self.file_menu.addAction("&Open Tracks")
|
||||
self.import_track_action = self.file_menu.addAction("&Import Tracks")
|
||||
|
||||
self.playlist_menu = QMenu("&Playlist", self.menu_bar)
|
||||
self.menu_bar.addMenu(self.playlist_menu)
|
||||
|
||||
self.open_playlist_action = self.playlist_menu.addAction("&Open Playlist")
|
||||
self.import_playlist_action = self.playlist_menu.addAction("&Import Playlist")
|
||||
|
||||
self.edit_menu = QMenu("&Edit", self.menu_bar)
|
||||
self.menu_bar.addMenu(self.edit_menu)
|
||||
|
||||
self.view_menu = QMenu("&View", self.menu_bar)
|
||||
self.menu_bar.addMenu(self.view_menu)
|
||||
self.settings_action = self.edit_menu.addAction("&Settings")
|
||||
|
||||
self.track_control = TrackControl(app)
|
||||
self.addToolBar(self.track_control)
|
||||
|
@ -48,16 +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.settings_action.triggered.connect(self.settings.show)
|
||||
|
||||
self.track_info = TrackInfo(app)
|
||||
self.addToolBar(Qt.ToolBarArea.BottomToolBarArea, self.track_info)
|
||||
|
||||
dock_menu = self.createPopupMenu()
|
||||
dock_menu.setTitle("Docks And Toolbars")
|
||||
self.view_menu.addMenu(dock_menu)
|
||||
|
||||
close_shortcut = QShortcut("Ctrl+Q", self)
|
||||
close_shortcut.activated.connect(self.close)
|
||||
|
|
153
wobuzz/ui/playlist.py
Normal file
153
wobuzz/ui/playlist.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
from PyQt6.QtGui import QDropEvent, QIcon, QFont
|
||||
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame
|
||||
|
||||
from .track import TrackItem
|
||||
|
||||
|
||||
class PlaylistView(QTreeWidget):
|
||||
itemDropped = pyqtSignal(QTreeWidget, list)
|
||||
|
||||
def __init__(self, playlist, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.playlist = playlist
|
||||
self.app = playlist.app
|
||||
|
||||
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)
|
||||
|
||||
self.playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
|
||||
|
||||
headers = [
|
||||
"#",
|
||||
"Title",
|
||||
"Artist",
|
||||
"Album",
|
||||
"# Custom Sorting"
|
||||
]
|
||||
|
||||
self.setHeaderLabels(headers)
|
||||
|
||||
self.load_tracks()
|
||||
|
||||
self.itemActivated.connect(self.on_track_activation)
|
||||
|
||||
def on_user_sort(self):
|
||||
num_tracks = self.topLevelItemCount()
|
||||
|
||||
i = 0
|
||||
|
||||
while i < num_tracks:
|
||||
track_item = self.topLevelItem(i)
|
||||
track = track_item.track
|
||||
|
||||
track_item.index_user_sort = i
|
||||
track_item.index = i
|
||||
|
||||
track_item.setText(5, str(i + 1))
|
||||
|
||||
self.playlist.tracks[i] = track
|
||||
|
||||
track.set_occurrences()
|
||||
|
||||
i += 1
|
||||
|
||||
if self.app.player.current_playlist.has_tracks():
|
||||
self.app.player.cache_next_track()
|
||||
|
||||
def dropEvent(self, event: QDropEvent):
|
||||
# receive items that were dropped and create new items from its tracks (new items bc. widgets can only have
|
||||
# one parent)
|
||||
if event.source() == self:
|
||||
items = self.selectedItems() # dragged items are always selected items
|
||||
|
||||
self.itemDropped.emit(self, items)
|
||||
|
||||
else:
|
||||
items = self.app.gui.dropped
|
||||
|
||||
i = 0
|
||||
|
||||
for item in items:
|
||||
track = item.track
|
||||
|
||||
self.playlist.tracks.append(track)
|
||||
|
||||
track_item = TrackItem(track, i, self)
|
||||
|
||||
i += 1
|
||||
|
||||
super().dropEvent(event)
|
||||
|
||||
event.accept()
|
||||
|
||||
self.on_user_sort()
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
# store dragged items in gui.dropped, so the other playlist can receive it
|
||||
if event.source() == self:
|
||||
items = self.selectedItems()
|
||||
|
||||
self.app.gui.dropped = items
|
||||
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
event.accept()
|
||||
|
||||
def load_tracks(self):
|
||||
i = 0
|
||||
|
||||
for track in self.playlist.tracks:
|
||||
track_item = TrackItem(track, i, self)
|
||||
|
||||
i += 1
|
||||
|
||||
def on_track_activation(self, item, column):
|
||||
if not self.app.player.current_playlist == self.playlist:
|
||||
self.app.player.current_playlist = self.playlist
|
||||
|
||||
index = self.indexOfTopLevelItem(item)
|
||||
self.app.player.play_track_in_playlist(index)
|
||||
|
||||
def on_track_change(self, previous_track, track):
|
||||
# unmark the previous track and playlist and mark the current track and playlist as playing
|
||||
|
||||
playlist_tabs = self.parent().parent()
|
||||
index = playlist_tabs.indexOf(self) # tab index of this playlist
|
||||
|
||||
if previous_track:
|
||||
# unmark all playlists by looping through the tabs
|
||||
for i in range(playlist_tabs.count()):
|
||||
playlist_tabs.setTabIcon(i, QIcon(None))
|
||||
|
||||
# unmark the previous track in all playlists
|
||||
for item in previous_track.items:
|
||||
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:
|
||||
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
|
||||
|
||||
# mark the current track in this playlist
|
||||
item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
|
||||
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):
|
||||
TrackItem(track, self.topLevelItemCount() - 1, self)
|
||||
|
|
@ -32,17 +32,10 @@ class PlaylistTabBar(QTabBar):
|
|||
playlist_view = self.tab_widget.widget(index)
|
||||
playlist = playlist_view.playlist
|
||||
|
||||
if not playlist.loaded:
|
||||
playlist.load()
|
||||
|
||||
self.app.gui.clicked_playlist = playlist
|
||||
|
||||
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)
|
||||
|
@ -58,7 +51,7 @@ class PlaylistTabBar(QTabBar):
|
|||
if index == -1: # when no tab was clicked, do nothing
|
||||
return
|
||||
|
||||
playlist_view = self.tab_widget.widget(index)
|
||||
playlist = playlist_view.playlist
|
||||
title = self.tabButton(index, QTabBar.ButtonPosition.RightSide)
|
||||
|
||||
self.context_menu.exec(event.globalPos(), title)
|
||||
|
||||
self.context_menu.exec(event.globalPos(), index, playlist)
|
||||
|
|
|
@ -4,8 +4,6 @@ from PyQt6.QtCore import QPoint
|
|||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWidgets import QMenu, QTabBar
|
||||
|
||||
from .tab_title_editor import TabTitleEditor
|
||||
|
||||
|
||||
class PlaylistContextMenu(QMenu):
|
||||
def __init__(self, parent=None):
|
||||
|
@ -13,8 +11,7 @@ class PlaylistContextMenu(QMenu):
|
|||
|
||||
self.tab_bar: QTabBar = parent
|
||||
|
||||
self.tab_index = -1
|
||||
self.playlist = None
|
||||
self.playlist_title = None
|
||||
|
||||
self.title = self.addSection("Playlist Actions")
|
||||
|
||||
|
@ -27,20 +24,17 @@ class PlaylistContextMenu(QMenu):
|
|||
self.rename_action.triggered.connect(self.rename)
|
||||
self.delete_action.triggered.connect(self.delete)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def exec(self, pos: QPoint, index: int, playlist):
|
||||
self.tab_index = index
|
||||
self.playlist = playlist
|
||||
def exec(self, pos: QPoint, title):
|
||||
self.playlist_title = title
|
||||
|
||||
self.title.setText(playlist.title)
|
||||
self.title.setText(title.text()) # set section title
|
||||
|
||||
super().exec(pos)
|
||||
|
||||
def rename(self):
|
||||
# create temporary QLineEdit for renaming the tab
|
||||
title_editor = TabTitleEditor(self.playlist, self.tab_bar, self.tab_index)
|
||||
|
||||
self.tab_bar.setTabButton(self.tab_index, QTabBar.ButtonPosition.RightSide, title_editor)
|
||||
self.playlist_title.setFocus()
|
||||
|
||||
def delete(self):
|
||||
self.playlist.delete()
|
||||
self.playlist_title.playlist_view.playlist.delete()
|
||||
self.playlist_title.playlist_view.deleteLater()
|
||||
|
||||
|
|
41
wobuzz/ui/playlist_tabs/tab_title.py
Normal file
41
wobuzz/ui/playlist_tabs/tab_title.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QMouseEvent
|
||||
from PyQt6.QtWidgets import QLineEdit
|
||||
|
||||
from .tab_bar import PlaylistTabBar
|
||||
|
||||
|
||||
class TabTitle(QLineEdit):
|
||||
def __init__(self, app, label, parent, index: int, playlist_view):
|
||||
super().__init__(label, parent)
|
||||
|
||||
self.app = app
|
||||
self.tab_bar: PlaylistTabBar = parent
|
||||
self.index = index
|
||||
self.playlist_view = playlist_view
|
||||
|
||||
self.setStyleSheet("QLineEdit {background: transparent;}")
|
||||
|
||||
self.setFocusPolicy(Qt.FocusPolicy.TabFocus)
|
||||
|
||||
self.editingFinished.connect(self.on_edit)
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
||||
self.tab_bar.tabBarDoubleClicked.emit(self.index)
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent):
|
||||
self.tab_bar.tabBarClicked.emit(self.index)
|
||||
self.tab_bar.setCurrentIndex(self.index)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.tab_bar.contextMenuEvent(event, self)
|
||||
|
||||
def on_edit(self):
|
||||
self.clearFocus()
|
||||
|
||||
self.playlist_view.playlist.rename(self.text())
|
||||
|
||||
self.setText(self.playlist_view.playlist.title)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QLineEdit, QTabBar
|
||||
|
||||
|
||||
class TabTitleEditor(QLineEdit):
|
||||
def __init__(self, playlist, parent, index: int):
|
||||
super().__init__(playlist.title, parent)
|
||||
|
||||
self.playlist = playlist
|
||||
self.tab_bar = parent
|
||||
self.index = index
|
||||
|
||||
self.tab_bar.setTabText(index, "")
|
||||
|
||||
self.setFocus()
|
||||
|
||||
self.editingFinished.connect(self.on_edit)
|
||||
|
||||
def on_edit(self):
|
||||
self.playlist.rename(self.text())
|
||||
|
||||
self.deleteLater()
|
||||
self.tab_bar.setTabButton(self.index, QTabBar.ButtonPosition.RightSide, None)
|
||||
self.tab_bar.setTabText(self.index, self.playlist.title)
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QTabWidget
|
||||
from PyQt6.QtWidgets import QTabWidget, QTabBar
|
||||
|
||||
from .tab_bar import PlaylistTabBar
|
||||
from .tab_title import TabTitle
|
||||
|
||||
|
||||
class PlaylistTabs(QTabWidget):
|
||||
|
@ -18,3 +19,13 @@ class PlaylistTabs(QTabWidget):
|
|||
|
||||
self.setMovable(True)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def addTab(self, widget, label):
|
||||
super().addTab(widget, None)
|
||||
|
||||
index = self.tab_bar.count() - 1
|
||||
|
||||
title = TabTitle(self.app, label, self.tab_bar, index, widget)
|
||||
|
||||
self.tab_bar.setTabButton(index, QTabBar.ButtonPosition.RightSide, title)
|
||||
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QDropEvent, QIcon
|
||||
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
|
||||
|
||||
from .track import TrackItem
|
||||
from ..wobuzzm3u import WBZM3UData
|
||||
|
||||
|
||||
class PlaylistView(QTreeWidget):
|
||||
itemDropped = pyqtSignal(QTreeWidget, list)
|
||||
sort_signal = pyqtSignal(int, Qt.SortOrder)
|
||||
|
||||
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
|
||||
|
||||
def __init__(self, playlist, library_widget, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.playlist = playlist
|
||||
self.library_widget = library_widget
|
||||
|
||||
self.app = playlist.app
|
||||
|
||||
self.header = self.header()
|
||||
self.header.setSectionsClickable(True)
|
||||
self.header.setSortIndicatorShown(True)
|
||||
|
||||
playlist.views[id(self.library_widget)] = self # let the playlist know that this view exists
|
||||
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
self.setColumnCount(5)
|
||||
|
||||
headers = [
|
||||
"#",
|
||||
"Title",
|
||||
"Artist",
|
||||
"Album",
|
||||
"Genre",
|
||||
"# Custom Sorting"
|
||||
]
|
||||
|
||||
self.setHeaderLabels(headers)
|
||||
|
||||
self.itemActivated.connect(self.on_track_activation)
|
||||
self.header.sectionClicked.connect(self.on_header_click)
|
||||
self.sort_signal.connect(self.sortItems)
|
||||
|
||||
def on_header_click(self, section_index: int):
|
||||
if section_index == 0: # this would just invert the current sorting
|
||||
return
|
||||
|
||||
sorting = self.playlist.sorting
|
||||
last_order = sorting[4]
|
||||
|
||||
if last_order.sort_by + 1 == section_index:
|
||||
order = WBZM3UData.SortOrder(last_order.sort_by, not last_order.ascending) # invert order on 2nd click
|
||||
|
||||
self.playlist.sorting[4] = order # set sorting
|
||||
|
||||
# convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder
|
||||
qorder = Qt.SortOrder.AscendingOrder if order.ascending else Qt.SortOrder.DescendingOrder
|
||||
|
||||
self.header.setSortIndicator(section_index, qorder)
|
||||
|
||||
else:
|
||||
del sorting[0] # remove first sort
|
||||
|
||||
# last sort is this section index + 1, ascending
|
||||
sorting.append(WBZM3UData.SortOrder(section_index - 1, True))
|
||||
|
||||
self.header.setSortIndicator(section_index, Qt.SortOrder.AscendingOrder)
|
||||
|
||||
self.sort()
|
||||
|
||||
def sort(self):
|
||||
for order in self.playlist.sorting:
|
||||
# convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder
|
||||
qorder = Qt.SortOrder.AscendingOrder if order.ascending else Qt.SortOrder.DescendingOrder
|
||||
|
||||
# somehow, QTreeWidget.sortItems() cant be called from a thread, so we have to use a signal to execute it
|
||||
# in the main thread
|
||||
self.sort_signal.emit(order.sort_by + 1, qorder)
|
||||
# self.sortItems(index, qorder)
|
||||
|
||||
self.on_sort()
|
||||
|
||||
def on_sort(self, user_sort: bool=False):
|
||||
num_tracks = self.topLevelItemCount()
|
||||
|
||||
i = 0
|
||||
|
||||
while i < num_tracks:
|
||||
track = self.topLevelItem(i)
|
||||
|
||||
i += 1
|
||||
|
||||
track.setText(0, str(i)) # 0 = index
|
||||
|
||||
if user_sort:
|
||||
track.setText(5, str(i)) # 5 = user sort index
|
||||
|
||||
if user_sort:
|
||||
# set last sort to user sort
|
||||
if not self.playlist.sorting[4].sort_by == WBZM3UData.SortOrder.custom_sorting:
|
||||
del self.playlist.sorting[0]
|
||||
|
||||
self.playlist.sorting.append(WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True))
|
||||
|
||||
self.header.setSortIndicator(5, Qt.SortOrder.AscendingOrder)
|
||||
|
||||
self.playlist.sync(self, user_sort) # sync playlist to this view
|
||||
|
||||
def dropEvent(self, event: QDropEvent):
|
||||
# receive items that were dropped and create new items from its tracks (new items bc. widgets can only have
|
||||
# one parent)
|
||||
if event.source() == self:
|
||||
items = self.selectedItems() # dragged items are always selected items
|
||||
|
||||
self.itemDropped.emit(self, items)
|
||||
|
||||
else:
|
||||
items = self.app.gui.dropped
|
||||
|
||||
i = 0
|
||||
|
||||
for item in items:
|
||||
track = item.track
|
||||
|
||||
self.playlist.tracks.append(track)
|
||||
|
||||
track_item = TrackItem(track, i, self)
|
||||
|
||||
i += 1
|
||||
|
||||
super().dropEvent(event)
|
||||
|
||||
event.accept()
|
||||
|
||||
self.on_sort(True)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
# store dragged items in gui.dropped, so the other playlist can receive it
|
||||
if event.source() == self:
|
||||
items = self.selectedItems()
|
||||
|
||||
self.app.gui.dropped = items
|
||||
|
||||
super().dragEnterEvent(event)
|
||||
|
||||
event.accept()
|
||||
|
||||
def load_tracks(self):
|
||||
i = 0
|
||||
|
||||
for track in self.playlist.tracks:
|
||||
track_item = TrackItem(track, i, self)
|
||||
|
||||
i += 1
|
||||
|
||||
def on_track_activation(self, item, column):
|
||||
if not self.app.player.current_playlist == self.playlist:
|
||||
self.app.player.current_playlist = self.playlist
|
||||
|
||||
index = self.indexOfTopLevelItem(item)
|
||||
self.app.player.play_track_in_playlist(index)
|
||||
|
||||
def on_track_change(self, previous_track, track):
|
||||
# unmark the previous track and playlist and mark the current track and playlist as playing
|
||||
|
||||
playlist_tabs = self.parent().parent()
|
||||
index = playlist_tabs.indexOf(self) # tab index of this playlist
|
||||
|
||||
if previous_track:
|
||||
# unmark all playlists by looping through the tabs
|
||||
for i in range(playlist_tabs.count()):
|
||||
playlist_tabs.setTabIcon(i, QIcon(None))
|
||||
|
||||
# unmark the previous track in all playlists
|
||||
for item in previous_track.items:
|
||||
item.unmark()
|
||||
|
||||
if track:
|
||||
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
|
||||
|
||||
# mark the current track in this playlist
|
||||
item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
|
||||
item.mark()
|
||||
|
||||
def append_track(self, track):
|
||||
TrackItem(track, self.topLevelItemCount(), self)
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QDialog, QFileDialog
|
||||
|
||||
from .library.import_dialog import ImportDialog
|
||||
from ..types import Types
|
||||
|
||||
|
||||
class Popups:
|
||||
def __init__(self, app, gui):
|
||||
self.app = app
|
||||
self.gui = gui
|
||||
|
||||
self.window = gui.window
|
||||
|
||||
self.audio_file_selector = QFileDialog(self.window, "Select Audio Files")
|
||||
self.audio_file_selector.setFileMode(QFileDialog.FileMode.ExistingFiles)
|
||||
self.audio_file_selector.setNameFilters(["Audio Files (*.flac *.wav *.mp3 *.ogg *.opus *.m4a)", "Any (*)"])
|
||||
self.audio_file_selector.setViewMode(QFileDialog.ViewMode.List)
|
||||
|
||||
self.playlist_file_selector = QFileDialog(self.window, "Select Playlist")
|
||||
self.playlist_file_selector.setFileMode(QFileDialog.FileMode.ExistingFile)
|
||||
self.playlist_file_selector.setNameFilters(["Playlists (*.wbz.m3u *.m3u)", "Any (*)"])
|
||||
self.playlist_file_selector.setViewMode(QFileDialog.ViewMode.List)
|
||||
|
||||
self.import_dialog = ImportDialog()
|
||||
|
||||
self.window.open_track_action.triggered.connect(self.open_tracks)
|
||||
self.window.import_track_action.triggered.connect(self.import_tracks)
|
||||
self.window.open_playlist_action.triggered.connect(self.open_playlist)
|
||||
self.window.import_playlist_action.triggered.connect(self.import_playlist)
|
||||
|
||||
def select_audio_files(self):
|
||||
if self.audio_file_selector.exec():
|
||||
return self.audio_file_selector.selectedFiles()
|
||||
|
||||
def select_playlist_file(self):
|
||||
if self.playlist_file_selector.exec():
|
||||
return self.playlist_file_selector.selectedFiles()[0]
|
||||
|
||||
def open_tracks(self):
|
||||
files = self.select_audio_files()
|
||||
|
||||
if files is not None and not files == []:
|
||||
self.app.library.open_tracks(files)
|
||||
|
||||
def get_import_options(self):
|
||||
import_options = Types.ImportOptions()
|
||||
|
||||
if self.import_dialog.tagging_section.overwrite_metadata.isChecked():
|
||||
artist = self.import_dialog.tagging_section.artist.text()
|
||||
album = self.import_dialog.tagging_section.album.text()
|
||||
genre = self.import_dialog.tagging_section.genre.text()
|
||||
|
||||
if not artist == "":
|
||||
import_options.artist = artist
|
||||
|
||||
if not album == "":
|
||||
import_options.album = album
|
||||
|
||||
if not genre == "":
|
||||
import_options.genre = genre
|
||||
|
||||
if self.import_dialog.file_section.copy_type_copy.isChecked():
|
||||
import_options.copy_type = Types.CopyType.copy
|
||||
|
||||
elif self.import_dialog.file_section.copy_type_move.isChecked():
|
||||
import_options.copy_type = Types.CopyType.move
|
||||
|
||||
return import_options
|
||||
|
||||
def import_tracks(self):
|
||||
files = self.select_audio_files()
|
||||
|
||||
if files is None or files == []:
|
||||
return
|
||||
|
||||
if self.import_dialog.exec() == QDialog.rejected:
|
||||
return
|
||||
|
||||
import_options = self.get_import_options()
|
||||
|
||||
self.app.library.import_tracks(files, import_options)
|
||||
|
||||
def open_playlist(self):
|
||||
playlist_path = self.select_playlist_file()
|
||||
|
||||
if playlist_path is not None and not playlist_path == "":
|
||||
self.app.library.open_playlist(playlist_path)
|
||||
|
||||
def import_playlist(self):
|
||||
playlist_path = self.select_playlist_file()
|
||||
|
||||
if playlist_path is None or playlist_path == "":
|
||||
return
|
||||
|
||||
if self.import_dialog.exec() == QDialog.rejected:
|
||||
return
|
||||
|
||||
import_options = self.get_import_options()
|
||||
|
||||
self.app.library.import_playlist(playlist_path, import_options)
|
|
@ -1 +0,0 @@
|
|||
#!/usr/bin/python3
|
|
@ -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())
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtWidgets import QWidget, QDockWidget, QScrollArea, QVBoxLayout
|
||||
|
||||
from .process import BackgroundProcess
|
||||
|
||||
|
||||
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.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()
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from .settings import Settings
|
||||
from .settings import Settings
|
14
wobuzz/ui/settings/behavior.py
Normal file
14
wobuzz/ui/settings/behavior.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QWidget, QFormLayout, QCheckBox
|
||||
|
||||
|
||||
class BehaviourSettings(QWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.layout = QFormLayout(self)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.clear_track_cache = QCheckBox(self)
|
||||
self.layout.addRow("Clear track cache immediately when finished", self.clear_track_cache)
|
|
@ -1,31 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtWidgets import QWidget, QScrollArea, QVBoxLayout
|
||||
|
||||
|
||||
class Category(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
|
||||
self.settings_container = QWidget(self.scroll_area)
|
||||
self.settings_layout = QVBoxLayout(self.settings_container)
|
||||
|
||||
# spacer widget to create a sort of list where the subcategory-spacing doesn't depend on the window height
|
||||
spacer_widget = QWidget(self)
|
||||
|
||||
self.settings_layout.addWidget(spacer_widget)
|
||||
|
||||
self.settings_container.setLayout(self.settings_layout)
|
||||
|
||||
self.scroll_area.setWidget(self.settings_container)
|
||||
|
||||
self.layout.addWidget(self.scroll_area)
|
||||
|
||||
def add_sub_category(self, sub_category):
|
||||
self.settings_layout.insertWidget(self.settings_layout.count() - 1, sub_category)
|
17
wobuzz/ui/settings/file.py
Normal file
17
wobuzz/ui/settings/file.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtGui import QPalette
|
||||
from PyQt6.QtWidgets import QWidget, QLineEdit, QFormLayout
|
||||
|
||||
|
||||
class FileSettings(QWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.layout = QFormLayout(self)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.library_path_input = QLineEdit(self)
|
||||
|
||||
self.layout.addRow("Library Path:", self.library_path_input)
|
||||
|
|
@ -1,20 +1,9 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget,
|
||||
QDockWidget,
|
||||
QTabWidget,
|
||||
QLineEdit,
|
||||
QCheckBox,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
QSizePolicy
|
||||
)
|
||||
|
||||
from .category import Category
|
||||
from .sub_category import SubCategory
|
||||
from PyQt6.QtWidgets import QWidget, QDockWidget, QTabWidget, QPushButton, QVBoxLayout
|
||||
from .file import FileSettings
|
||||
from .behavior import BehaviourSettings
|
||||
|
||||
|
||||
class Settings(QDockWidget):
|
||||
|
@ -38,77 +27,12 @@ class Settings(QDockWidget):
|
|||
self.tabs = QTabWidget(self.content)
|
||||
self.content_layout.addWidget(self.tabs)
|
||||
|
||||
self.file_settings = Category()
|
||||
|
||||
self.file_settings.paths = SubCategory("Paths")
|
||||
self.file_settings.add_sub_category(self.file_settings.paths)
|
||||
|
||||
self.file_settings.paths.library_path_input = QLineEdit()
|
||||
self.file_settings.paths.add_setting("Library Path:", self.file_settings.paths.library_path_input)
|
||||
|
||||
self.file_settings = FileSettings()
|
||||
self.tabs.addTab(self.file_settings, "Files")
|
||||
|
||||
self.behavior_settings = Category()
|
||||
|
||||
self.behavior_settings.playlist = SubCategory("Playlist",)
|
||||
self.behavior_settings.add_sub_category(self.behavior_settings.playlist)
|
||||
|
||||
self.behavior_settings.playlist.load_on_start = QCheckBox()
|
||||
self.behavior_settings.playlist.add_setting("Load on start:", self.behavior_settings.playlist.load_on_start)
|
||||
|
||||
self.behavior_settings.track = SubCategory("Track",)
|
||||
self.behavior_settings.add_sub_category(self.behavior_settings.track)
|
||||
|
||||
self.behavior_settings.track.clear_cache = QCheckBox()
|
||||
self.behavior_settings.track.add_setting(
|
||||
"Clear cache:",
|
||||
self.behavior_settings.track.clear_cache,
|
||||
"Automatically clear the track's cache after it finished. This greatly reduces RAM usage."
|
||||
)
|
||||
|
||||
self.behavior_settings = BehaviourSettings()
|
||||
self.tabs.addTab(self.behavior_settings, "Behavior")
|
||||
|
||||
self.appearance_settings = Category()
|
||||
|
||||
self.appearance_settings.track_info = SubCategory("Track Info")
|
||||
self.appearance_settings.add_sub_category(self.appearance_settings.track_info)
|
||||
|
||||
self.appearance_settings.track_info.cover_size = QSpinBox()
|
||||
self.appearance_settings.track_info.cover_size.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
self.appearance_settings.track_info.cover_size.setRange(16, 128)
|
||||
self.appearance_settings.track_info.cover_size.setSuffix("px")
|
||||
self.appearance_settings.track_info.cover_size.setSingleStep(10)
|
||||
self.appearance_settings.track_info.add_setting(
|
||||
"Album Cover Size",
|
||||
self.appearance_settings.track_info.cover_size,
|
||||
"The size of the album cover. (aspect-ratio: 1:1)"
|
||||
)
|
||||
|
||||
self.tabs.addTab(self.appearance_settings, "Appearance")
|
||||
|
||||
self.performance_settings = Category()
|
||||
|
||||
# self.performance_settings.memory = SubCategory("Memory", "Memory related settings")
|
||||
# self.performance_settings.add_sub_category(self.performance_settings.memory)
|
||||
|
||||
self.performance_settings.cpu = SubCategory("CPU",)
|
||||
self.performance_settings.add_sub_category(self.performance_settings.cpu)
|
||||
|
||||
self.performance_settings.cpu.gui_update_rate = QSpinBox()
|
||||
self.performance_settings.cpu.gui_update_rate.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
self.performance_settings.cpu.gui_update_rate.setRange(1, 60)
|
||||
self.performance_settings.cpu.gui_update_rate.setSuffix(" FPS")
|
||||
self.performance_settings.cpu.gui_update_rate.setSingleStep(5)
|
||||
self.performance_settings.cpu.add_setting(
|
||||
"GUI update rate:",
|
||||
self.performance_settings.cpu.gui_update_rate,
|
||||
"The rate at which gui-elements like the track-progress-slider get updated.\n"
|
||||
"Values above 20 don't really make sense for most monitors.\n"
|
||||
"Decreasing this value will reduce the CPU usage."
|
||||
)
|
||||
|
||||
self.tabs.addTab(self.performance_settings, "Performance")
|
||||
|
||||
self.save_button = QPushButton("&Save", self.content)
|
||||
self.content_layout.addWidget(self.save_button)
|
||||
|
||||
|
@ -118,33 +42,18 @@ class Settings(QDockWidget):
|
|||
self.save_button.pressed.connect(self.write_settings)
|
||||
|
||||
def update_all(self, _=True): # ignore visible parameter passed by visibilityChanged event
|
||||
self.file_settings.paths.library_path_input.setText(self.app.settings.library_path)
|
||||
self.behavior_settings.track.clear_cache.setChecked(self.app.settings.clear_track_cache)
|
||||
self.behavior_settings.playlist.load_on_start.setChecked(self.app.settings.load_on_start)
|
||||
self.performance_settings.cpu.gui_update_rate.setValue(self.app.settings.gui_update_rate)
|
||||
self.appearance_settings.track_info.cover_size.setValue(self.app.settings.album_cover_size)
|
||||
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)
|
||||
|
||||
def update_settings(self, key, value):
|
||||
match key:
|
||||
case "library_path":
|
||||
self.file_settings.paths.library_path_input.setText(value)
|
||||
self.file_settings.library_path_input.setText(value)
|
||||
|
||||
case "clear_track_cache":
|
||||
self.behavior_settings.track.clear_cache.setDown(value)
|
||||
|
||||
case "load_on_start":
|
||||
self.behavior_settings.playlist.load_on_start.setChecked(value)
|
||||
|
||||
case "gui_update_rate":
|
||||
self.performance_settings.cpu.gui_update_rate.setValue(value)
|
||||
|
||||
case "track_cover_size":
|
||||
self.appearance_settings.track_info.cover_size.setValue(value)
|
||||
self.behavior_settings.clear_track_cache.setDown(value)
|
||||
|
||||
def write_settings(self):
|
||||
self.app.settings.library_path = self.file_settings.paths.library_path_input.text()
|
||||
self.app.settings.clear_track_cache = self.behavior_settings.track.clear_cache.isChecked()
|
||||
self.app.settings.load_on_start = self.behavior_settings.playlist.load_on_start.isChecked()
|
||||
self.app.settings.gui_update_rate = self.performance_settings.cpu.gui_update_rate.value()
|
||||
self.app.settings.album_cover_size = self.appearance_settings.track_info.cover_size.value()
|
||||
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()
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QFont
|
||||
from PyQt6.QtWidgets import QLabel, QSizePolicy, QFormLayout
|
||||
|
||||
from ..custom_widgets import GroupBox
|
||||
|
||||
|
||||
class SubCategory(GroupBox):
|
||||
description_font = QFont()
|
||||
description_font.setPointSize(8)
|
||||
|
||||
def __init__(self, title: str, description: str=None, parent=None):
|
||||
super().__init__(title, parent)
|
||||
|
||||
self.layout = QFormLayout()
|
||||
self.setLayout(self.layout)
|
||||
|
||||
if description is not None:
|
||||
self.description = QLabel(description + "\n", self)
|
||||
self.layout.addRow(self.description)
|
||||
|
||||
def add_setting(self, text: str, setting, description: str=None):
|
||||
self.layout.addRow(text, setting)
|
||||
|
||||
if description is not None:
|
||||
description_label = QLabel(" " + description.replace("\n", "\n "))
|
||||
description_label.setFont(self.description_font)
|
||||
self.layout.addRow(description_label)
|
||||
|
|
@ -1,32 +1,20 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QFont, QIcon, QPalette
|
||||
from PyQt6.QtWidgets import 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):
|
||||
super().__init__(parent)
|
||||
|
||||
self.track = track
|
||||
self.index_user_sort = index
|
||||
|
||||
self.index = index
|
||||
self.parent = parent
|
||||
|
||||
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.set_occurrences()
|
||||
|
@ -38,34 +26,8 @@ class TrackItem(QTreeWidgetItem):
|
|||
)
|
||||
|
||||
self.setText(0, str(self.index + 1))
|
||||
self.setText(1, track.metadata.title)
|
||||
self.setText(2, track.metadata.artist)
|
||||
self.setText(3, track.metadata.album)
|
||||
self.setText(4, track.metadata.genre)
|
||||
self.setText(5, 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.bold_font)
|
||||
self.setFont(4, self.bold_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)
|
||||
self.setFont(4, self.normal_font)
|
||||
|
||||
def __lt__(self, other):
|
||||
# make numeric strings get sorted the right way
|
||||
|
||||
column = self.parent.sortColumn()
|
||||
|
||||
if column == 0 or column == 5:
|
||||
return int(self.text(column)) < int(other.text(column))
|
||||
|
||||
else:
|
||||
return super().__lt__(other)
|
||||
self.setText(1, track.tags.title)
|
||||
self.setText(2, track.tags.artist)
|
||||
self.setText(3, track.tags.album)
|
||||
self.setText(4, str(self.index_user_sort + 1))
|
||||
|
||||
|
|
|
@ -18,18 +18,14 @@ class TrackControl(QToolBar):
|
|||
|
||||
icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipBackward)
|
||||
self.previous_button = self.addAction(icon, "Previous")
|
||||
self.previous_button.setShortcut("Shift+Left")
|
||||
|
||||
self.toggle_play_button = self.addAction(self.play_icon, "Play/Pause")
|
||||
self.toggle_play_button.setShortcut("Space")
|
||||
|
||||
icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStop)
|
||||
self.stop_button = self.addAction(icon, "Stop")
|
||||
self.stop_button.setShortcut("Shift+S")
|
||||
|
||||
icon = QIcon.fromTheme(QIcon.ThemeIcon.MediaSkipForward)
|
||||
self.next_button = self.addAction(icon, "Next")
|
||||
self.next_button.setShortcut("Shift+Right")
|
||||
|
||||
self.progress_indicator = QLabel("0:00")
|
||||
self.addWidget(self.progress_indicator)
|
||||
|
@ -46,15 +42,19 @@ class TrackControl(QToolBar):
|
|||
def connect(self):
|
||||
self.previous_button.triggered.connect(self.previous_track)
|
||||
self.toggle_play_button.triggered.connect(self.toggle_playing)
|
||||
self.stop_button.triggered.connect(self.app.player.stop)
|
||||
self.stop_button.triggered.connect(self.stop)
|
||||
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.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:
|
||||
|
|
|
@ -1,113 +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.setWindowTitle("Track Info")
|
||||
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.setMargin(4)
|
||||
self.set_size(self.app.settings.album_cover_size)
|
||||
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.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
|
||||
self.title.setFont(self.title_font)
|
||||
info_container_layout.addWidget(self.title)
|
||||
|
||||
self.artist = QLabel("Artist", self.info_container)
|
||||
self.artist.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
|
||||
self.artist.setFont(self.artist_font)
|
||||
info_container_layout.addWidget(self.artist)
|
||||
|
||||
self.album = QLabel("Album", self.info_container)
|
||||
self.album.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
|
||||
self.album.setFont(self.album_font)
|
||||
info_container_layout.addWidget(self.album)
|
||||
|
||||
# spacer widget that makes the label spacing not depend on the container's height
|
||||
spacer_widget = QWidget(self.info_container)
|
||||
info_container_layout.addWidget(spacer_widget)
|
||||
|
||||
def update_info(self):
|
||||
current_playlist = self.app.player.current_playlist
|
||||
|
||||
if current_playlist is not None and current_playlist.current_track is not None:
|
||||
current_track = current_playlist.current_track
|
||||
title = current_track.metadata.title
|
||||
artist = current_track.metadata.artist
|
||||
album = current_track.metadata.album
|
||||
|
||||
self.title.setText(title)
|
||||
|
||||
if artist is not None and not artist == "":
|
||||
self.artist.setText(f"By {artist}")
|
||||
|
||||
else:
|
||||
self.artist.setText("")
|
||||
|
||||
if album is not None and not album == "":
|
||||
self.album.setText(f"In {album}")
|
||||
|
||||
else:
|
||||
self.album.setText("")
|
||||
|
||||
if current_track.metadata.images is None: # can't display cover image when there are no images at all
|
||||
self.track_cover.setPixmap(self.wobuzz_logo)
|
||||
|
||||
return
|
||||
|
||||
cover = current_track.metadata.images.any
|
||||
|
||||
if cover is None: # can't display cover image when there 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)
|
||||
|
||||
else:
|
||||
self.title.setText("No Playing Track")
|
||||
self.artist.setText("")
|
||||
self.album.setText("")
|
||||
self.track_cover.setPixmap(self.wobuzz_logo)
|
||||
|
||||
def set_size(self, size: int):
|
||||
self.track_cover.setFixedSize(size, size)
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QMouseEvent
|
||||
from PyQt6.QtWidgets import QSlider, QStyle, QStyleOptionSlider
|
||||
|
||||
PROGRESS_UPDATE_RATE = 60
|
||||
PROGRESS_UPDATE_INTERVAL = 1000 // PROGRESS_UPDATE_RATE
|
||||
|
||||
|
||||
class TrackProgressSlider(QSlider):
|
||||
def __init__(self, app, parent=None):
|
||||
|
@ -14,6 +17,10 @@ class TrackProgressSlider(QSlider):
|
|||
|
||||
self.dragged = False
|
||||
|
||||
self.progress_update_timer = QTimer()
|
||||
self.progress_update_timer.timeout.connect(self.update_progress)
|
||||
self.progress_update_timer.start(PROGRESS_UPDATE_INTERVAL)
|
||||
|
||||
option = QStyleOptionSlider()
|
||||
style = self.style()
|
||||
|
||||
|
@ -22,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):
|
||||
|
@ -50,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):
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
def __getattr__(name):
|
||||
match name:
|
||||
case "WobuzzM3U":
|
||||
from .wobuzzm3u import WobuzzM3U
|
||||
|
||||
return WobuzzM3U
|
||||
|
||||
case "WBZM3UData":
|
||||
from .wbzm3u_data import WBZM3UData
|
||||
|
||||
return WBZM3UData
|
|
@ -1,68 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
|
||||
class WBZM3UData:
|
||||
is_comment = False
|
||||
type: "WBZM3UData"
|
||||
|
||||
class Header:
|
||||
is_comment = True
|
||||
|
||||
class Path(str):
|
||||
pass
|
||||
|
||||
class URL(str):
|
||||
pass
|
||||
|
||||
class SortOrder:
|
||||
is_comment = True
|
||||
|
||||
track_title = 0
|
||||
track_artist = 1
|
||||
track_album = 2
|
||||
track_genre = 3
|
||||
custom_sorting = 4
|
||||
|
||||
def __init__(self, sort_by: int, ascending: bool):
|
||||
self.sort_by = sort_by
|
||||
self.ascending = ascending
|
||||
|
||||
class TrackMetadata:
|
||||
class TrackTitle(str):
|
||||
is_comment = True
|
||||
|
||||
class TrackArtist(str):
|
||||
is_comment = True
|
||||
|
||||
class TrackAlbum(str):
|
||||
is_comment = True
|
||||
|
||||
class TrackGenre(str):
|
||||
is_comment = True
|
||||
|
||||
|
||||
class WBZM3UData(WBZM3UData):
|
||||
class Header(WBZM3UData.Header, WBZM3UData):
|
||||
pass
|
||||
|
||||
class Path(WBZM3UData.Path, WBZM3UData, str):
|
||||
pass
|
||||
|
||||
class URL(WBZM3UData.URL, WBZM3UData, str):
|
||||
pass
|
||||
|
||||
class SortOrder(WBZM3UData.SortOrder, WBZM3UData):
|
||||
pass
|
||||
|
||||
class TrackMetadata(WBZM3UData.TrackMetadata, WBZM3UData):
|
||||
class TrackTitle(WBZM3UData.TrackMetadata.TrackTitle, WBZM3UData.TrackMetadata, str):
|
||||
pass
|
||||
|
||||
class TrackArtist(WBZM3UData.TrackMetadata.TrackArtist, WBZM3UData.TrackMetadata, str):
|
||||
pass
|
||||
|
||||
class TrackAlbum(WBZM3UData.TrackMetadata.TrackAlbum, WBZM3UData.TrackMetadata, str):
|
||||
pass
|
||||
|
||||
class TrackGenre(WBZM3UData.TrackMetadata.TrackGenre, str):
|
||||
pass
|
|
@ -1,87 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from . import WBZM3UData
|
||||
|
||||
|
||||
class WobuzzM3U:
|
||||
sort_orders = {
|
||||
"Title": WBZM3UData.SortOrder.track_title,
|
||||
"Artist": WBZM3UData.SortOrder.track_artist,
|
||||
"Album": WBZM3UData.SortOrder.track_album,
|
||||
"Genre": WBZM3UData.SortOrder.track_genre,
|
||||
"Custom": WBZM3UData.SortOrder.custom_sorting
|
||||
}
|
||||
|
||||
sort_order_names = {
|
||||
WBZM3UData.SortOrder.track_title: "Title",
|
||||
WBZM3UData.SortOrder.track_artist: "Artist",
|
||||
WBZM3UData.SortOrder.track_album: "Album",
|
||||
WBZM3UData.SortOrder.track_genre: "Genre",
|
||||
WBZM3UData.SortOrder.custom_sorting: "Custom"
|
||||
}
|
||||
|
||||
def __init__(self, filename: str):
|
||||
self.filename = filename
|
||||
|
||||
def parse_line(self, line: str) -> WBZM3UData | None:
|
||||
if line.startswith("#"): # comments and EXTM3U/WOBUZZM3U
|
||||
if line.startswith("#WOBUZZM3U"):
|
||||
return WBZM3UData.Header()
|
||||
|
||||
elif line.startswith("#SORT: "): # sort
|
||||
sorting_params = line[6:] # delete "#SORT: " from the line
|
||||
|
||||
sorting = sorting_params.split(", ") # split into the sort column specifier and the sort order
|
||||
# e.g. ["Title", "Ascending"]
|
||||
|
||||
if not sorting[0] in self.sort_orders:
|
||||
return None
|
||||
|
||||
sort_by = self.sort_orders[sorting[0]]
|
||||
order = sorting[1] == "Ascending"
|
||||
|
||||
return WBZM3UData.SortOrder(sort_by, order)
|
||||
|
||||
elif line.startswith("#TRACK_TITLE: "):
|
||||
return WBZM3UData.TrackMetadata.TrackTitle(line[14:])
|
||||
|
||||
elif line.startswith("#TRACK_ARTIST: "):
|
||||
return WBZM3UData.TrackMetadata.TrackArtist(line[15:])
|
||||
|
||||
elif line.startswith("#TRACK_ALBUM: "):
|
||||
return WBZM3UData.TrackMetadata.TrackAlbum(line[14:])
|
||||
|
||||
return None
|
||||
|
||||
elif line.startswith("http"):
|
||||
return WBZM3UData.URL("URLs currently aren't supported.")
|
||||
|
||||
# line contains a path
|
||||
return WBZM3UData.Path(line)
|
||||
|
||||
def assemble_line(self, data: WBZM3UData) -> str | None:
|
||||
if data is WBZM3UData.Header or isinstance(data, WBZM3UData.Header):
|
||||
return "#WOBUZZM3U\n"
|
||||
|
||||
if isinstance(data, WBZM3UData.Path):
|
||||
return f"{data}\n"
|
||||
|
||||
if isinstance(data, WBZM3UData.URL):
|
||||
return None
|
||||
|
||||
if isinstance(data, WBZM3UData.SortOrder):
|
||||
direction = "Ascending" if data.ascending else "Descending"
|
||||
|
||||
return f"#SORT: {self.sort_order_names[data.sort_by]}, {direction}\n"
|
||||
|
||||
if isinstance(data, WBZM3UData.TrackMetadata.TrackTitle):
|
||||
return f"#TRACK_TITLE: {data}\n"
|
||||
|
||||
if isinstance(data, WBZM3UData.TrackMetadata.TrackArtist):
|
||||
return f"#TRACK_ARTIST: {data}\n"
|
||||
|
||||
if isinstance(data, WBZM3UData.TrackMetadata.TrackAlbum):
|
||||
return f"#TRACK_ALBUM: {data}\n"
|
||||
|
||||
if isinstance(data, WBZM3UData.TrackMetadata.TrackGenre):
|
||||
return f"#TRACK_GENRE: {data}\n"
|
Loading…
Add table
Reference in a new issue