diff --git a/wobuzz/player/playlist.py b/wobuzz/player/playlist.py index c93ba9e..694ee4c 100644 --- a/wobuzz/player/playlist.py +++ b/wobuzz/player/playlist.py @@ -5,7 +5,10 @@ import threading from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QAbstractItemView +from reactivex.observable.case import case_ + from .track import Track, TrackMetadata +from ..wobuzzm3u import WobuzzM3U, WBZM3UData class Playlist: @@ -22,14 +25,13 @@ class Playlist: # no other playlist can be created using the same name self.app.utils.unique_names.append(self.title) - # the number is the index of the header section, - # 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) + # 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.tracks: list[Track] = [] self.current_track_index = 0 @@ -160,37 +162,39 @@ class Playlist: lambda: i ) + wbzm3u = WobuzzM3U(self.path) track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U 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 + line_data = wbzm3u.parse_line(line) - sorting = sort_line.split(", ") # split into the sort column specifier and the sort order - # e.g. ["0", "True"] + 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 - # convert these from strings back to int and bool and append them to the sorting - self.sorting.append((int(sorting[0]), sorting[1] == "True")) + self.sorting.append(line_data) - elif line.startswith("#TRACK_TITLE: "): - track_metadata.title = line[14:] + if isinstance(line_data, WBZM3UData.TrackMetadata.TrackTitle): + track_metadata.title = line_data - elif line.startswith("#TRACK_ARTIST: "): - track_metadata.artist = line[15:] + if isinstance(line_data, WBZM3UData.TrackMetadata.TrackArtist): + track_metadata.artist = line_data - elif line.startswith("#TRACK_ALBUM: "): - track_metadata.album = line[14:] + if isinstance(line_data, WBZM3UData.TrackMetadata.TrackAlbum): + track_metadata.album = line_data i += 1 continue - elif line.startswith("http"): # ignore urls + elif isinstance(line_data, WBZM3UData.URL): # ignore urls i += 1 continue @@ -283,18 +287,21 @@ class Playlist: first_view.sortItems(4, Qt.SortOrder.AscendingOrder) self.sync(first_view) - wbz_data = "#WOBUZZM3U\n" # header + wbzm3u = WobuzzM3U(self.path) - for sort_column, order in self.sorting: - wbz_data += f"#SORT: {sort_column}, {order}\n" + wbz_data = "" + + for order in self.sorting: + wbz_data += wbzm3u.assemble_line(order) for track in self.tracks: # cache track metadata - wbz_data += f"#TRACK_TITLE: {track.metadata.title}\n" - wbz_data += f"#TRACK_ARTIST: {track.metadata.artist}\n" - wbz_data += f"#TRACK_ALBUM: {track.metadata.album}\n" + 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.write(wbz_data) diff --git a/wobuzz/ui/playlist_view.py b/wobuzz/ui/playlist_view.py index acfbdcc..6f09edd 100644 --- a/wobuzz/ui/playlist_view.py +++ b/wobuzz/ui/playlist_view.py @@ -5,6 +5,7 @@ from PyQt6.QtGui import QDropEvent, QIcon from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView from .track import TrackItem +from ..wobuzzm3u import WBZM3UData class PlaylistView(QTreeWidget): @@ -50,34 +51,36 @@ class PlaylistView(QTreeWidget): return sorting = self.playlist.sorting - last_sort_section_index, order = sorting[4] + last_order = sorting[4] - if last_sort_section_index == section_index: - order = not order # invert order + 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] = (section_index, order) # set sorting + self.playlist.sorting[4] = order # set sorting # convert True/False to Qt.SortOrder.AscendingOrder/Qt.SortOrder.DescendingOrder - qorder = Qt.SortOrder.AscendingOrder if order else 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 - sorting.append((section_index, True)) # last sort is this section index, ascending + + # 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 index, order in self.playlist.sorting: + for 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 + 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(index, qorder) + self.sort_signal.emit(order.sort_by + 1, qorder) # self.sortItems(index, qorder) self.on_sort() @@ -98,10 +101,11 @@ class PlaylistView(QTreeWidget): 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 + # 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((4, True)) + self.playlist.sorting.append(WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True)) self.header.setSortIndicator(4, Qt.SortOrder.AscendingOrder) diff --git a/wobuzz/wobuzzm3u/__init__.py b/wobuzz/wobuzzm3u/__init__.py new file mode 100644 index 0000000..54b7ee8 --- /dev/null +++ b/wobuzz/wobuzzm3u/__init__.py @@ -0,0 +1,13 @@ +#!/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 diff --git a/wobuzz/wobuzzm3u/wbzm3u_data.py b/wobuzz/wobuzzm3u/wbzm3u_data.py new file mode 100644 index 0000000..5d1c49c --- /dev/null +++ b/wobuzz/wobuzzm3u/wbzm3u_data.py @@ -0,0 +1,68 @@ +#!/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 diff --git a/wobuzz/wobuzzm3u/wobuzzm3u.py b/wobuzz/wobuzzm3u/wobuzzm3u.py new file mode 100644 index 0000000..c536483 --- /dev/null +++ b/wobuzz/wobuzzm3u/wobuzzm3u.py @@ -0,0 +1,87 @@ +#!/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 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"