Added tools for reading and writing WOBUZZM3U and made the sorting parameters human readable.
This commit is contained in:
parent
f7995aee9e
commit
9e20e21e6f
5 changed files with 219 additions and 40 deletions
|
@ -5,7 +5,10 @@ import threading
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtWidgets import QAbstractItemView
|
from PyQt6.QtWidgets import QAbstractItemView
|
||||||
|
from reactivex.observable.case import case_
|
||||||
|
|
||||||
from .track import Track, TrackMetadata
|
from .track import Track, TrackMetadata
|
||||||
|
from ..wobuzzm3u import WobuzzM3U, WBZM3UData
|
||||||
|
|
||||||
|
|
||||||
class Playlist:
|
class Playlist:
|
||||||
|
@ -22,14 +25,13 @@ 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,
|
# sort order
|
||||||
# the bool is the sorting order (True = ascending, False = descending)
|
self.sorting: list[WBZM3UData.SortOrder] = [
|
||||||
self.sorting: list[tuple[int, bool]] = [
|
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_title, True),
|
||||||
(0, True),
|
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_artist, True),
|
||||||
(1, True),
|
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_album, True),
|
||||||
(2, True),
|
WBZM3UData.SortOrder(WBZM3UData.SortOrder.track_genre, True),
|
||||||
(3, True),
|
WBZM3UData.SortOrder(WBZM3UData.SortOrder.custom_sorting, True)
|
||||||
(4, True)
|
|
||||||
]
|
]
|
||||||
self.tracks: list[Track] = []
|
self.tracks: list[Track] = []
|
||||||
self.current_track_index = 0
|
self.current_track_index = 0
|
||||||
|
@ -160,37 +162,39 @@ class Playlist:
|
||||||
lambda: i
|
lambda: i
|
||||||
)
|
)
|
||||||
|
|
||||||
|
wbzm3u = WobuzzM3U(self.path)
|
||||||
track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U
|
track_metadata = TrackMetadata() # cached track metadata from WOBUZZM3U
|
||||||
|
|
||||||
while i < num_lines:
|
while i < num_lines:
|
||||||
line = lines[i]
|
line = lines[i]
|
||||||
|
|
||||||
if line.startswith("#"): # comments and EXTM3U/WOBUZZM3U
|
line_data = wbzm3u.parse_line(line)
|
||||||
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
|
if line_data is None:
|
||||||
# e.g. ["0", "True"]
|
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
|
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(line_data)
|
||||||
self.sorting.append((int(sorting[0]), sorting[1] == "True"))
|
|
||||||
|
|
||||||
elif line.startswith("#TRACK_TITLE: "):
|
if isinstance(line_data, WBZM3UData.TrackMetadata.TrackTitle):
|
||||||
track_metadata.title = line[14:]
|
track_metadata.title = line_data
|
||||||
|
|
||||||
elif line.startswith("#TRACK_ARTIST: "):
|
if isinstance(line_data, WBZM3UData.TrackMetadata.TrackArtist):
|
||||||
track_metadata.artist = line[15:]
|
track_metadata.artist = line_data
|
||||||
|
|
||||||
elif line.startswith("#TRACK_ALBUM: "):
|
if isinstance(line_data, WBZM3UData.TrackMetadata.TrackAlbum):
|
||||||
track_metadata.album = line[14:]
|
track_metadata.album = line_data
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif line.startswith("http"): # ignore urls
|
elif isinstance(line_data, WBZM3UData.URL): # ignore urls
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
@ -283,18 +287,21 @@ class Playlist:
|
||||||
first_view.sortItems(4, Qt.SortOrder.AscendingOrder)
|
first_view.sortItems(4, Qt.SortOrder.AscendingOrder)
|
||||||
self.sync(first_view)
|
self.sync(first_view)
|
||||||
|
|
||||||
wbz_data = "#WOBUZZM3U\n" # header
|
wbzm3u = WobuzzM3U(self.path)
|
||||||
|
|
||||||
for sort_column, order in self.sorting:
|
wbz_data = ""
|
||||||
wbz_data += f"#SORT: {sort_column}, {order}\n"
|
|
||||||
|
for order in self.sorting:
|
||||||
|
wbz_data += wbzm3u.assemble_line(order)
|
||||||
|
|
||||||
for track in self.tracks:
|
for track in self.tracks:
|
||||||
# cache track metadata
|
# cache track metadata
|
||||||
wbz_data += f"#TRACK_TITLE: {track.metadata.title}\n"
|
wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackTitle(track.metadata.title))
|
||||||
wbz_data += f"#TRACK_ARTIST: {track.metadata.artist}\n"
|
wbz_data += wbzm3u.assemble_line(WBZM3UData.TrackMetadata.TrackArtist(track.metadata.artist))
|
||||||
wbz_data += f"#TRACK_ALBUM: {track.metadata.album}\n"
|
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(self.path, "w")
|
||||||
wbz.write(wbz_data)
|
wbz.write(wbz_data)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from PyQt6.QtGui import QDropEvent, QIcon
|
||||||
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
|
from PyQt6.QtWidgets import QTreeWidget, QAbstractItemView
|
||||||
|
|
||||||
from .track import TrackItem
|
from .track import TrackItem
|
||||||
|
from ..wobuzzm3u import WBZM3UData
|
||||||
|
|
||||||
|
|
||||||
class PlaylistView(QTreeWidget):
|
class PlaylistView(QTreeWidget):
|
||||||
|
@ -50,34 +51,36 @@ class PlaylistView(QTreeWidget):
|
||||||
return
|
return
|
||||||
|
|
||||||
sorting = self.playlist.sorting
|
sorting = self.playlist.sorting
|
||||||
last_sort_section_index, order = sorting[4]
|
last_order = sorting[4]
|
||||||
|
|
||||||
if last_sort_section_index == section_index:
|
if last_order.sort_by + 1 == section_index:
|
||||||
order = not order # invert order
|
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
|
# 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)
|
self.header.setSortIndicator(section_index, qorder)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
del sorting[0] # remove first sort
|
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.header.setSortIndicator(section_index, Qt.SortOrder.AscendingOrder)
|
||||||
|
|
||||||
self.sort()
|
self.sort()
|
||||||
|
|
||||||
def sort(self):
|
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
|
# 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
|
# somehow, QTreeWidget.sortItems() cant be called from a thread, so we have to use a signal to execute it
|
||||||
# in the main thread
|
# in the main thread
|
||||||
self.sort_signal.emit(index, qorder)
|
self.sort_signal.emit(order.sort_by + 1, qorder)
|
||||||
# self.sortItems(index, qorder)
|
# self.sortItems(index, qorder)
|
||||||
|
|
||||||
self.on_sort()
|
self.on_sort()
|
||||||
|
@ -98,10 +101,11 @@ class PlaylistView(QTreeWidget):
|
||||||
track.setText(4, str(i)) # 4 = user sort index
|
track.setText(4, str(i)) # 4 = user sort index
|
||||||
|
|
||||||
if user_sort:
|
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]
|
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)
|
self.header.setSortIndicator(4, Qt.SortOrder.AscendingOrder)
|
||||||
|
|
||||||
|
|
13
wobuzz/wobuzzm3u/__init__.py
Normal file
13
wobuzz/wobuzzm3u/__init__.py
Normal file
|
@ -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
|
68
wobuzz/wobuzzm3u/wbzm3u_data.py
Normal file
68
wobuzz/wobuzzm3u/wbzm3u_data.py
Normal file
|
@ -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
|
87
wobuzz/wobuzzm3u/wobuzzm3u.py
Normal file
87
wobuzz/wobuzzm3u/wobuzzm3u.py
Normal file
|
@ -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"
|
Loading…
Add table
Reference in a new issue