modularize src

+ merge main.py and manage.py
+ move start logic to swingmusic/__main__.py
+ add a run.py on the project root
This commit is contained in:
cwilvx
2025-05-25 20:35:54 +03:00
parent 76fc97e088
commit 86fabcd5e3
171 changed files with 658 additions and 627 deletions
+30
View File
@@ -0,0 +1,30 @@
from swingmusic.db.userdata import MixTable
from swingmusic.plugins.mixes import MixesPlugin
def find_mix(mixid: str, sourcehash: str):
"""
Find a mix in the homepage store or the db.
"""
from swingmusic.store.homepage import HomepageStore
mixtype = "custom_mixes" if mixid[0] == "t" else "artist_mixes"
# INFO: Try getting the mix from the homepage store
mix = HomepageStore.get_mix(mixtype, mixid)
if mix and mix["sourcehash"] == sourcehash:
return mix
# INFO: Get the mix from the db
mix = MixTable.get_by_sourcehash(sourcehash)
if not mix:
return None
if mixtype == "custom_mixes":
mix = MixesPlugin.get_track_mix(mix)
if not mix:
return None
return mix.to_dict()
+158
View File
@@ -0,0 +1,158 @@
import os
from swingmusic.db.userdata import PlaylistTable
from swingmusic.lib.home import find_mix
from swingmusic.lib.home.recentlyadded import get_recently_added_playlist
from swingmusic.lib.home.recentlyplayed import get_recently_played_playlist
from swingmusic.models.logger import TrackLog
from swingmusic.store.albums import AlbumStore
from swingmusic.store.artists import ArtistStore
from swingmusic.store.tracks import TrackStore
def create_items(entries: list[TrackLog], limit: int):
custom_playlists = [
{"name": "recentlyadded", "handler": get_recently_added_playlist},
{"name": "recentlyplayed", "handler": get_recently_played_playlist},
]
items = []
added = set()
for entry in entries:
if len(items) >= limit:
break
if entry.source in added:
continue
added.add(entry.source)
if entry.type == "mix":
if not entry.type_src:
continue
splits = entry.type_src.split(".")
try:
mixid = splits[0]
sourcehash = splits[1]
except IndexError:
continue
# INFO: Get mix from homepage store
mix = find_mix(mixid, sourcehash)
if not mix:
continue
items.append(
{
"type": "mix",
"hash": entry.type_src,
"timestamp": entry.timestamp,
}
)
continue
if entry.type == "album":
album = AlbumStore.albummap.get(entry.type_src)
if album is None:
continue
item = {
"type": "album",
"hash": entry.type_src,
"timestamp": entry.timestamp,
}
items.append(item)
continue
if entry.type == "artist":
artist = ArtistStore.artistmap.get(entry.type_src)
if artist is None:
continue
items.append(
{
"type": "artist",
"hash": entry.type_src,
"timestamp": entry.timestamp,
}
)
continue
if entry.type == "folder":
folder = entry.type_src
if not folder:
continue
if not folder.endswith("/"):
folder += "/"
is_home_dir = entry.type_src == "$home"
if is_home_dir:
folder = os.path.expanduser("~")
item = {
"type": "folder",
"hash": entry.type_src,
"timestamp": entry.timestamp,
}
items.append(item)
continue
if entry.type == "playlist":
is_custom = entry.type_src in [i["name"] for i in custom_playlists]
if is_custom:
items.append(
{
"type": "playlist",
"hash": entry.type_src,
"timestamp": entry.timestamp,
"is_custom": True,
}
)
continue
playlist = PlaylistTable.get_by_id(entry.type_src)
if playlist is None:
continue
item = {
"type": "playlist",
"hash": entry.type_src,
"timestamp": entry.timestamp,
}
items.append(item)
continue
if entry.type == "favorite":
items.append(
{
"type": "favorite",
"timestamp": entry.timestamp,
}
)
continue
t = TrackStore.trackhashmap.get(entry.trackhash)
if t is None:
continue
item = {
"type": "track",
"hash": entry.trackhash,
"timestamp": entry.timestamp,
}
items.append(item)
return items
@@ -0,0 +1,42 @@
from swingmusic.db.userdata import ScrobbleTable
from swingmusic.lib.home.create_items import create_items
from swingmusic.models.logger import TrackLog
def get_recently_played(
limit: int, userid: int | None = None, _entries: list[TrackLog] = []
):
"""
Get the recently played items for the homepage.
Pass a list of track log entries to use a subset of the scrobble table.
"""
# TODO: Paginate this
items = []
BATCH_SIZE = 200
current_index = 0
if len(_entries):
entries = _entries
limit = 1
else:
entries = ScrobbleTable.get_all(0, BATCH_SIZE, userid=userid)
max_iterations = 20
iterations = 0
while len(items) < limit and iterations < max_iterations:
items.extend(create_items(entries, limit))
current_index += BATCH_SIZE
if len(items) < limit:
entries = ScrobbleTable.get_all(
start=current_index + 1, limit=BATCH_SIZE, userid=userid
)
if not entries:
break
iterations += 1
return items
+213
View File
@@ -0,0 +1,213 @@
from datetime import datetime
from swingmusic.lib.playlistlib import get_first_4_images
from swingmusic.models.playlist import Playlist
from swingmusic.models.track import Track
from swingmusic.store.tracks import TrackStore
from swingmusic.store.albums import AlbumStore
from swingmusic.store.artists import ArtistStore
from itertools import groupby
from swingmusic.utils import flatten
from swingmusic.utils.dates import (
create_new_date,
date_string_to_time_passed,
)
older_albums = set()
older_artists = set()
def calc_based_on_percent(items: list[str], total: int):
"""
Checks if items is more than 85% of total items. Returns a boolean and the most common item.
"""
most_common = max(items, key=items.count)
most_common_count = items.count(most_common)
return most_common_count / total >= 0.7, most_common, most_common_count
def check_is_album_folder(tracks: list[Track]):
albumhashes = [t.albumhash for t in tracks]
return calc_based_on_percent(albumhashes, len(tracks))
def check_is_artist_folder(tracks: list[Track]):
# INFO: flatten artist hashes using "-" as a separator
artisthashes = flatten([t.artisthashes for t in tracks])
return calc_based_on_percent(artisthashes, len(tracks))
def check_is_track_folder(tracks: list[Track]):
# INFO: is more of a playlist
if len(tracks) >= 3:
return False
return [create_track(t) for t in tracks]
def create_track(t: Track):
"""
Creates a recently added track entry.
"""
return {
"type": "track",
"hash": t.trackhash,
"timestamp": t.last_mod,
"help_text": "NEW TRACK",
}
# INFO: Keys: folder, tracks, time (timestamp)
# group_type = dict[str, str | list[Track] | float]
def check_folder_type(group_: dict):
# check if all tracks in group have the same albumhash
# if so, return "album"
key: str = group_["folder"]
tracks: list[Track] = group_["tracks"]
time: float = group_["time"]
existing_artist_hashes: set[str] = set(ArtistStore.artistmap.keys())
existing_album_hashes: set[str] = set(AlbumStore.albummap.keys())
if len(tracks) == 1:
entry = create_track(tracks[0])
entry["timestamp"] = time
return entry
is_album, albumhash, _ = check_is_album_folder(tracks)
if is_album:
# album = AlbumTable.get_album_by_albumhash(albumhash)
entry = AlbumStore.albummap.get(albumhash)
if entry is None:
return None
return {
"type": "album",
"hash": albumhash,
"timestamp": time,
"help_text": (
"NEW ALBUM" if albumhash in existing_album_hashes else "NEW TRACKS"
),
}
is_artist, artisthash, trackcount = check_is_artist_folder(tracks)
if is_artist:
entry = ArtistStore.artistmap.get(artisthash)
if entry is None:
return None
return {
"type": "artist",
"hash": artisthash,
"timestamp": time,
"help_text": (
"NEW ARTIST" if artisthash not in existing_artist_hashes else "NEW MUSIC"
),
}
is_track_folder = check_is_track_folder(tracks)
return (
is_track_folder
if is_track_folder
else {
"type": "folder",
"hash": key,
"timestamp": time,
"help_text": "NEW MUSIC",
}
)
def group_track_by_folders(tracks: list[Track], groups: dict[str, list[Track]]):
"""
Groups tracks by folder and returns a list of groups sorted by last modified date.
Uses generator expressions to avoid creating intermediate lists.
"""
# INFO: sort tracks by folder name, then group by folder name
tracks = sorted(tracks, key=lambda t: t.folder)
thisgroup = groupby(tracks, lambda t: t.folder)
for folder, thistracks in thisgroup:
groups.setdefault(folder, []).extend(thistracks)
return groups
def get_recently_added_items(limit: int = 7):
tracks = get_recently_added_tracks(start=0, limit=None)
groups = group_track_by_folders(tracks, {})
grouplist = []
# INFO: sort tracks by last modified date in descending order to get the most recent last modified date
for folder, trackgroup in groups.items():
trackgroup.sort(key=lambda t: t.last_mod, reverse=True)
grouplist.append(
{
"folder": folder,
"len": len(trackgroup),
"tracks": trackgroup,
"time": trackgroup[0].last_mod,
}
)
# sort groups by last modified date
grouplist = sorted(grouplist, key=lambda group: group["time"], reverse=True)
recent_items = []
for group in grouplist:
item = check_folder_type(group)
if item not in recent_items:
if not item:
continue
(
recent_items.append(item)
if type(item) == dict
else recent_items.extend(item)
)
if len(recent_items) >= limit:
break
return recent_items
def get_recently_added_playlist(limit: int = 100):
playlist = Playlist(
id="recentlyadded",
name="Recently Added",
image=None,
last_updated="Now",
settings={},
trackhashes=[],
)
tracks = get_recently_added_tracks(limit=limit)
try:
# Create date to show as last updated
date = datetime.fromtimestamp(tracks[0].last_mod)
except IndexError:
return playlist, []
playlist._last_updated = date_string_to_time_passed(create_new_date(date))
images = get_first_4_images(tracks=tracks)
playlist.images = images
playlist.duration = sum(t.duration for t in tracks)
playlist.count = len(tracks)
return playlist, tracks
def get_recently_added_tracks(start: int = 0, limit: int | None = 100):
return TrackStore.get_recently_added(start, limit)
+30
View File
@@ -0,0 +1,30 @@
from datetime import datetime
from swingmusic.models.playlist import Playlist
from swingmusic.lib.playlistlib import get_first_4_images
from swingmusic.utils.dates import (
create_new_date,
date_string_to_time_passed,
)
from swingmusic.store.tracks import TrackStore
def get_recently_played_playlist(limit: int = 100):
playlist = Playlist(
id="recentlyplayed",
name="Recently Played",
image=None,
last_updated="Now",
settings={},
trackhashes=[],
)
tracks = TrackStore.get_recently_played(limit)
date = datetime.fromtimestamp(tracks[0].lastplayed)
playlist._last_updated = date_string_to_time_passed(create_new_date(date))
images = get_first_4_images(tracks=tracks)
playlist.images = images
return playlist, tracks
+161
View File
@@ -0,0 +1,161 @@
from swingmusic.db.userdata import FavoritesTable, MixTable, PlaylistTable
from swingmusic.lib.home import find_mix
from swingmusic.lib.home.recentlyadded import get_recently_added_playlist
from swingmusic.lib.home.recentlyplayed import get_recently_played_playlist
from swingmusic.lib.playlistlib import get_first_4_images
from swingmusic.serializers.album import album_serializer
from swingmusic.serializers.artist import serialize_for_card
from swingmusic.serializers.playlist import serialize_for_card as serialize_playlist
from swingmusic.serializers.track import serialize_track
from swingmusic.store.albums import AlbumStore
from swingmusic.store.artists import ArtistStore
from swingmusic.store.folder import FolderStore
from swingmusic.store.tracks import TrackStore
from swingmusic.utils.dates import timestamp_to_time_passed
def recover_items(items: list[dict]):
custom_playlists = [
{"name": "recentlyadded", "handler": get_recently_added_playlist},
{"name": "recentlyplayed", "handler": get_recently_played_playlist},
]
recovered = []
for item in items:
recovered_item = None
if item["type"] == "album":
album = AlbumStore.get_album_by_hash(item["hash"])
if album is None:
continue
album = album_serializer(
album,
to_remove={
"genres",
"date",
"count",
"duration",
"albumartists_hashes",
"og_title",
},
)
recovered_item = {
"type": "album",
"item": album,
}
elif item["type"] == "artist":
artist = ArtistStore.get_artist_by_hash(item["hash"])
if artist is None:
continue
recovered_item = {
"type": "artist",
"item": serialize_for_card(artist),
}
elif item["type"] == "folder":
count = FolderStore.count_tracks_containing_paths([item["hash"]])
recovered_item = {
"type": "folder",
"item": {
"path": item["hash"],
"count": count[0]["trackcount"],
},
}
elif item["type"] == "playlist":
if item.get("is_custom"):
playlist, _ = next(
i["handler"]()
for i in custom_playlists
if i["name"] == item["hash"]
)
playlist.images = [i["image"] for i in playlist.images]
playlist = serialize_playlist(
playlist, to_remove={"settings", "duration"}
)
recovered_item = {
"type": "playlist",
"item": playlist,
}
else:
playlist = PlaylistTable.get_by_id(item["hash"])
if playlist is None:
continue
tracks = TrackStore.get_tracks_by_trackhashes(playlist.trackhashes)
playlist.clear_lists()
if not playlist.has_image:
images = get_first_4_images(tracks)
images = [i["image"] for i in images]
playlist.images = images
recovered_item = {
"type": "playlist",
"item": serialize_playlist(playlist),
}
elif item["type"] == "favorite":
image = None
last_trackhash = FavoritesTable.get_last_trackhash()
if last_trackhash:
trackhash = last_trackhash.replace("track_", "")
entry = TrackStore.trackhashmap.get(trackhash)
if entry:
image = entry.tracks[0].image
recovered_item = {
"type": "favorite",
"item": {
"count": FavoritesTable.count_tracks(),
"image": image,
},
}
elif item["type"] == "track":
track = TrackStore.trackhashmap.get(item["hash"])
if track is None:
continue
recovered_item = {
"type": "track",
"item": serialize_track(track.get_best()),
}
elif item["type"] == "mix":
try:
splits = item["hash"].split(".")
mixid = splits[0]
sourcehash = splits[1]
except IndexError:
continue
mix = find_mix(mixid, sourcehash)
if mix is None:
continue
recovered_item = {
"type": "mix",
"item": mix,
}
if recovered_item is not None:
helptext = item.get("help_text") or item.get("type")
secondary_text = item.get("secondary_text")
if "secondary_text" in item:
secondary_text = item["secondary_text"]
elif "timestamp" in item:
secondary_text = timestamp_to_time_passed(item["timestamp"])
if helptext:
recovered_item["item"]["help_text"] = helptext
if secondary_text:
recovered_item["item"]["time"] = secondary_text
recovered.append(recovered_item)
return recovered