Compare commits

..

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

8 changed files with 47 additions and 231 deletions

View file

@ -5,14 +5,11 @@ Currently, it just has really basic features but many more things are planned.
### Features ### Features
| Feature | Description | State | | Feature | Description | State |
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------| |------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> Implemented | | Playlists | You can create and load `.m3u` playlists, edit them and they will get stored on the disk automatically. | <input type="checkbox" disabled checked /> Implemented |
| Background Job Monitor | A QDockWidget where background processes are listed. | <input type="checkbox" disabled checked /> Implemented | | Background Job Monitor | A QDockWidget where background processes are listed. | <input type="checkbox" disabled checked /> Implemented |
| Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | <input type="checkbox" disabled /> Not Implemented | | Audio effects | Audio effects like normalizing and an equalizer. This can be implemented pretty easily because Wobuzz uses [Pydub](https://pydub.com/), which has these effects built in. | <input type="checkbox" disabled /> Not Implemented |
| Soundcloud downloader | A simple Soundcloud-downloader like maybe integrating [SCDL](https://pypi.org/project/scdl/) would be really cool. | <input type="checkbox" disabled /> Not Implemented |
| Synchronisation between devices | This should be pretty hard to implement and idk. if i will ever make it, but synchronisation could be pretty practical e.g. if you have multiple audio systems in different rooms. | <input type="checkbox" disabled /> Not Implemented |
| Audio visualization | Firstly, rather simple audio visualization like an oscilloscope would be cool, also something more complicated like [ProjectM](https://github.com/projectM-visualizer/projectm) could be integrated. | <input type="checkbox" disabled /> Not Implemented |
## Installation ## Installation
@ -26,7 +23,7 @@ there you can find the commands that you need for the installation.
You firstly have to install the newest dependencies: You firstly have to install the newest dependencies:
``` bash ``` bash
sudo apt install xcb libxcb-cursor0 ffmpeg python3-pip git sudo apt install xcb libxcb-cursor0 ffmpeg
``` ```
Now, you can install the newest unstable version using just one more command: Now, you can install the newest unstable version using just one more command:

View file

@ -26,7 +26,6 @@ setuptools.setup(
url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz", url="https://teapot.informationsanarchistik.de/Wobbl/Wobuzz",
author="The Wobbler", author="The Wobbler",
author_email="emil@i21k.de", author_email="emil@i21k.de",
license="GNU GPLv3",
packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]), packages=setuptools.find_packages(include=["wobuzz", "wobuzz.*"]),
package_data={"": ["*.txt", "*.svg"]}, package_data={"": ["*.txt", "*.svg"]},
install_requires=[ install_requires=[

View file

@ -22,15 +22,7 @@ class Playlist:
# no other playlist can be created using the same name # no other playlist can be created using the same name
self.app.utils.unique_names.append(self.title) self.app.utils.unique_names.append(self.title)
# the number is the index of the header section, self.sorting: list[Qt.SortOrder] | None = None # Custom sort order if None
# the bool is the sorting order (True = ascending, False = descending)
self.sorting: list[tuple[int, bool]] = [
(0, True),
(1, True),
(2, True),
(3, True),
(4, True)
]
self.tracks: list[Track] = [] self.tracks: list[Track] = []
self.current_track_index = 0 self.current_track_index = 0
self.current_track: Track | None = None self.current_track: Track | None = None
@ -134,87 +126,7 @@ class Playlist:
self.app.gui.on_background_job_stop(process_title) self.app.gui.on_background_job_stop(process_title)
def load_from_wbz(self, path): def load_from_wbz(self, path):
file = open(path, "r") self.load_from_m3u(path) # placeholder
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
)
while i < num_lines:
line = lines[i]
if line.startswith("#"): # comments and EXTM3U/WOBUZZM3U
if line.startswith("#SORT: "): # sort
sort_line = line[6:] # delete "#SORT: " from the line
sorting = sort_line.split(", ") # split into the sort column specifier and the sort order
# e.g. ["0", "True"]
del self.sorting[0] # delete first sort so the length stays at 6
# convert these from strings back to int and bool and append them to the sorting
self.sorting.append((int(sorting[0]), sorting[1] == "True"))
i += 1
continue
elif line.startswith("http"): # filter out urls
i += 1
continue
self.append_track(Track(self.app, line, cache=i == 0)) # first track is cached
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()
def has_tracks(self): def has_tracks(self):
return len(self.tracks) > 0 return len(self.tracks) > 0
@ -257,15 +169,7 @@ class Playlist:
return self.current_track.sound, self.current_track.duration return self.current_track.sound, self.current_track.duration
def save(self): def save(self):
wbz_data = ""
first_view = list(self.views.values())[0]
first_view.sortItems(4, Qt.SortOrder.AscendingOrder)
self.sync(first_view)
wbz_data = "#WOBUZZM3U\n" # header
for sort_column, order in self.sorting:
wbz_data += f"#SORT: {sort_column}, {order}\n"
for track in self.tracks: for track in self.tracks:
wbz_data += f"{track.path}\n" wbz_data += f"{track.path}\n"

View file

@ -67,7 +67,7 @@ class Track:
self.duration = len(self.audio) # track duration in milliseconds self.duration = len(self.audio) # track duration in milliseconds
self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images self.tags = TinyTag.get(self.path, ignore_errors=True, duration=False, image=True) # metadata with images
self.cached = True self.cached = True

View file

@ -1,17 +1,18 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QDropEvent, QIcon from PyQt6.QtGui import QDropEvent, QIcon, QFont
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView, QFrame
from .track import TrackItem from .track import TrackItem
class PlaylistView(QTreeWidget): class PlaylistView(QTreeWidget):
itemDropped = pyqtSignal(QTreeWidget, list) itemDropped = pyqtSignal(QTreeWidget, list)
sort_signal = pyqtSignal(int, Qt.SortOrder)
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart) normal_font = QFont()
bold_font = QFont()
bold_font.setBold(True)
def __init__(self, playlist, dock, parent=None): def __init__(self, playlist, dock, parent=None):
super().__init__(parent) super().__init__(parent)
@ -21,16 +22,14 @@ class PlaylistView(QTreeWidget):
self.app = playlist.app self.app = playlist.app
self.header = self.header()
self.header.setSectionsClickable(True)
self.header.setSortIndicatorShown(True)
playlist.views[id(dock)] = self playlist.views[id(dock)] = self
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setColumnCount(4) self.setColumnCount(4)
self.playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
headers = [ headers = [
"#", "#",
"Title", "Title",
@ -42,70 +41,30 @@ class PlaylistView(QTreeWidget):
self.setHeaderLabels(headers) self.setHeaderLabels(headers)
self.itemActivated.connect(self.on_track_activation) 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): def on_user_sort(self):
if section_index == 0: # this would just invert the current sorting
return
sorting = self.playlist.sorting
last_sort_section_index, order = sorting[4]
if last_sort_section_index == section_index:
order = not order # invert order
self.playlist.sorting[4] = (section_index, order) # set sorting
# convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder
qorder = Qt.SortOrder.AscendingOrder if order else Qt.SortOrder.DescendingOrder
self.header.setSortIndicator(section_index, qorder)
else:
del sorting[0] # remove first sort
sorting.append((section_index, True)) # last sort is this section index, ascending
self.header.setSortIndicator(section_index, Qt.SortOrder.AscendingOrder)
self.sort()
def sort(self):
for index, order in self.playlist.sorting:
# convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder
qorder = Qt.SortOrder.AscendingOrder if order 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(index, qorder)
# self.sortItems(index, qorder)
self.on_sort()
def on_sort(self, user_sort: bool=False):
num_tracks = self.topLevelItemCount() num_tracks = self.topLevelItemCount()
i = 0 i = 0
while i < num_tracks: while i < num_tracks:
track = self.topLevelItem(i) 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 i += 1
track.setText(0, str(i)) # 0 = index # make sure the next track is cached (could be moved by user)
if self.app.player.current_playlist == self.playlist and self.app.player.current_playlist.has_tracks():
if user_sort: self.app.player.cache_next_track()
track.setText(4, str(i)) # 4 = user sort index
if user_sort:
if not self.playlist.sorting[4][0] == 4: # set last sort to user sort
del self.playlist.sorting[0]
self.playlist.sorting.append((4, True))
self.header.setSortIndicator(4, Qt.SortOrder.AscendingOrder)
self.playlist.sync(self, user_sort) # sync playlist to this view
def dropEvent(self, event: QDropEvent): def dropEvent(self, event: QDropEvent):
# receive items that were dropped and create new items from its tracks (new items bc. widgets can only have # receive items that were dropped and create new items from its tracks (new items bc. widgets can only have
@ -133,7 +92,7 @@ class PlaylistView(QTreeWidget):
event.accept() event.accept()
self.on_sort(True) self.on_user_sort()
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
# store dragged items in gui.dropped, so the other playlist can receive it # store dragged items in gui.dropped, so the other playlist can receive it
@ -174,15 +133,21 @@ class PlaylistView(QTreeWidget):
# unmark the previous track in all playlists # unmark the previous track in all playlists
for item in previous_track.items: for item in previous_track.items:
item.unmark() item.setIcon(0, QIcon(None))
item.setFont(1, self.normal_font)
item.setFont(2, self.normal_font)
item.setFont(3, self.normal_font)
if track: if track:
playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist playlist_tabs.setTabIcon(index, self.playing_mark) # mark this playlist
# mark the current track in this playlist # mark the current track in this playlist
item = self.topLevelItem(self.app.player.current_playlist.current_track_index) item = self.topLevelItem(self.app.player.current_playlist.current_track_index)
item.mark() item.setIcon(0, self.playing_mark)
item.setFont(1, self.bold_font)
item.setFont(2, self.bold_font)
item.setFont(3, self.normal_font)
def append_track(self, track): def append_track(self, track):
TrackItem(track, self.topLevelItemCount(), self) TrackItem(track, self.topLevelItemCount() - 1, self)

View file

@ -1,32 +1,20 @@
#!/usr/bin/python3 #!/usr/bin/python3
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QIcon, QPalette
from PyQt6.QtWidgets import QTreeWidgetItem from PyQt6.QtWidgets import QTreeWidgetItem
class TrackItem(QTreeWidgetItem): class TrackItem(QTreeWidgetItem):
normal_font = QFont()
bold_font = QFont()
bold_font.setBold(True)
playing_mark = QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)
def __init__(self, track, index, parent=None): def __init__(self, track, index, parent=None):
super().__init__(parent) super().__init__(parent)
self.track = track self.track = track
self.index_user_sort = index self.index_user_sort = index
self.index = index self.index = index
self.parent = parent
self.playlist = parent.playlist self.playlist = parent.playlist
palette = parent.palette()
self.highlight_color = palette.color(QPalette.ColorRole.Highlight)
self.base_color = palette.color(QPalette.ColorRole.Base)
track.items.append(self) track.items.append(self)
track.set_occurrences() track.set_occurrences()
@ -43,26 +31,3 @@ class TrackItem(QTreeWidgetItem):
self.setText(3, track.tags.album) self.setText(3, track.tags.album)
self.setText(4, str(self.index_user_sort + 1)) self.setText(4, str(self.index_user_sort + 1))
def mark(self):
self.setIcon(0, self.playing_mark)
self.setFont(1, self.bold_font)
self.setFont(2, self.bold_font)
self.setFont(3, self.normal_font)
def unmark(self):
self.setIcon(0, QIcon(None))
self.setFont(1, self.normal_font)
self.setFont(2, self.normal_font)
self.setFont(3, self.normal_font)
def __lt__(self, other):
# make numeric strings get sorted the right way
column = self.parent.sortColumn()
if column == 0 or column == 4:
return int(self.text(column)) < int(other.text(column))
else:
return super().__lt__(other)

View file

@ -50,11 +50,12 @@ class TrackInfo(QToolBar):
def update_info(self): def update_info(self):
current_playlist = self.app.player.current_playlist current_playlist = self.app.player.current_playlist
if current_playlist is not None and current_playlist.current_track is not None: if current_playlist is not None:
current_track = current_playlist.current_track current_track = current_playlist.current_track
title = current_track.tags.title title = current_track.tags.title
artist = current_track.tags.artist artist = current_track.tags.artist
album = current_track.tags.album album = current_track.tags.album
cover_data = current_track.tags.images.any.data
self.title.setText(title) self.title.setText(title)
@ -70,15 +71,6 @@ class TrackInfo(QToolBar):
else: else:
self.album.setText("No Album") self.album.setText("No Album")
cover = current_track.tags.images.any
if cover is None:
self.track_cover.setPixmap(self.wobuzz_logo)
return
cover_data = cover.data
if isinstance(cover_data, bytes): if isinstance(cover_data, bytes):
cover_pixmap = QPixmap() cover_pixmap = QPixmap()
cover_pixmap.loadFromData(cover_data) cover_pixmap.loadFromData(cover_data)
@ -88,10 +80,4 @@ class TrackInfo(QToolBar):
else: else:
self.track_cover.setPixmap(self.wobuzz_logo) 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)

View file

@ -57,7 +57,7 @@ class TrackProgressSlider(QSlider):
def on_release(self): def on_release(self):
self.dragged = False self.dragged = False
if self.app.player.current_playlist is not None and self.app.player.current_playlist.has_tracks(): if self.app.player.current_playlist.has_tracks():
self.app.player.seek(self.value()) self.app.player.seek(self.value())
def update_progress(self): def update_progress(self):