try hashmap trackstore

This commit is contained in:
cwilvx
2024-07-07 16:52:18 +03:00
parent 2ba5d6c1d7
commit c116957982
8 changed files with 263 additions and 81 deletions
+3 -3
View File
@@ -129,7 +129,7 @@ class TrackTable(Base):
title: Mapped[str] = mapped_column(String())
track: Mapped[int] = mapped_column(Integer())
trackhash: Mapped[str] = mapped_column(String(), index=True)
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
# is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
lastplayed: Mapped[int] = mapped_column(Integer(), default=0)
playcount: Mapped[int] = mapped_column(Integer(), default=0)
playduration: Mapped[int] = mapped_column(Integer(), default=0)
@@ -270,7 +270,7 @@ class AlbumTable(Base):
og_title: Mapped[str] = mapped_column(String())
title: Mapped[str] = mapped_column(String())
trackcount: Mapped[int] = mapped_column(Integer())
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
# is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
lastplayed: Mapped[int] = mapped_column(Integer(), default=0)
playcount: Mapped[int] = mapped_column(Integer(), default=0)
playduration: Mapped[int] = mapped_column(Integer(), default=0)
@@ -360,7 +360,7 @@ class ArtistTable(Base):
genres: Mapped[str] = mapped_column(JSON())
name: Mapped[str] = mapped_column(String(), index=True)
trackcount: Mapped[int] = mapped_column(Integer())
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
# is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
lastplayed: Mapped[int] = mapped_column(Integer(), default=0)
playcount: Mapped[int] = mapped_column(Integer(), default=0)
playduration: Mapped[int] = mapped_column(Integer(), default=0)
+1 -1
View File
@@ -108,7 +108,7 @@ class GetFilesAndDirs:
tracks = []
if files:
tracks = TrackDB.get_tracks_by_filepaths(files)
tracks = list(FolderStore.get_tracks_by_filepaths(files))
folders = []
if not self.tracks_only:
+1 -1
View File
@@ -28,7 +28,7 @@ class Album:
og_title: str
title: str
trackcount: int
is_favorite: bool
# is_favorite: bool
lastplayed: int
playcount: int
playduration: int
+1 -1
View File
@@ -47,7 +47,7 @@ class Artist:
genrehashes: list[str]
name: str
trackcount: int
is_favorite: bool
# is_favorite: bool
lastplayed: int
playcount: int
playduration: int
+8 -7
View File
@@ -1,4 +1,6 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from app.utils.auth import get_current_userid
@dataclass(slots=True)
@@ -33,10 +35,14 @@ class Track:
playcount: int
playduration: int
is_favorite: bool = False
_pos: int = 0
_ati: str = ""
image: str = ""
fav_userids: set = field(default_factory=set)
@property
def is_favorite(self):
return get_current_userid() in self.fav_userids
def __post_init__(self):
self.image = self.albumhash + ".webp"
@@ -66,16 +72,11 @@ class Track:
# image: str = ""
# artist_hashes: str = ""
# fav_userids: list = field(default_factory=list)
# """
# A string of user ids separated by commas.
# """
# # is_favorite: bool = False
# @property
# def is_favorite(self):
# return current_user["id"] in self.fav_userids
# # temporary attributes
# _pos: int = 0 # for sorting tracks by disc and track number
# _ati: str = (
+1
View File
@@ -46,4 +46,5 @@ def load_into_mem():
# TrackStore.load_all_tracks(instance_key)
# AlbumStore.load_albums(instance_key)
# ArtistStore.load_artists(instance_key)
TrackStore.load_all_tracks(get_random_str())
FolderStore.load_filepaths()
+16
View File
@@ -2,6 +2,7 @@ from sortedcontainers import SortedSet
from concurrent.futures import ThreadPoolExecutor
from app.db.libdata import TrackTable
from app.store.tracks import TrackStore
class FolderStore:
@@ -13,6 +14,10 @@ class FolderStore:
"""
filepaths: SortedSet = SortedSet()
map: dict[str, str] = {}
"""
The map above is a dictionary that maps the folder path to the track hash, which can be used to fetch the track from the track store (a dict of track hashes to track objects).
"""
@classmethod
def load_filepaths(cls):
@@ -26,7 +31,18 @@ class FolderStore:
tracks = TrackTable.get_all()
for track in tracks:
cls.filepaths.add(track.filepath)
cls.map[track.filepath] = track.trackhash
@classmethod
def get_tracks_by_filepaths(cls, filepaths: list[str]):
for filepath in filepaths:
trackhash = cls.map.get(filepath)
if trackhash:
track = TrackStore.trackhashmap.get(trackhash)
if track:
yield [t for t in track.tracks if t.filepath == filepath][0]
@classmethod
def count_tracks_containing_paths(cls, paths: list[str]):
+232 -68
View File
@@ -1,18 +1,99 @@
# from tqdm import tqdm
import itertools
import sys
from typing import Callable
from flask_jwt_extended import current_user
from app.db.libdata import TrackTable
from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
from app.db.sqlite.tracks import SQLiteTrackMethods as trackdb
# from app.db.sqlite.tracks import SQLiteTrackMethods as trackdb
from app.db.userdata import FavoritesTable
from app.models import Track
from app.utils.bisection import use_bisection
from app.utils.customlist import CustomList
from app.utils.remove_duplicates import remove_duplicates
TRACKS_LOAD_KEY = ""
class TrackGroup:
"""
Tracks grouped under the same trackhash.
"""
def __init__(self, tracks: list[Track]):
self.tracks = tracks
def append(self, track: Track):
"""
Adds a track to the group.
"""
self.tracks.append(track)
def remove(self, track: Track):
"""
Removes a track from the group.
"""
self.tracks.remove(track)
def set_fav_userids(self, userids: set[int]):
"""
Sets the favorite userids.
"""
for track in self.tracks:
track.fav_userids = userids
def get_best(self):
"""
Returns the track with higest bitrate.
"""
return max(self.tracks, key=lambda x: x.bitrate)
def toggle_favorite(self, remove: bool = False):
"""
Adds a track to the favorites.
"""
userids = set(self.tracks[0].fav_userids)
if remove:
userids.remove(current_user["id"])
else:
userids.add(current_user["id"])
for track in self.tracks:
track.fav_userids = userids
def __len__(self):
return len(self.tracks)
class classproperty(property):
"""
A class property decorator.
"""
def __get__(self, owner_self, owner_cls):
return self.fget(owner_cls)
class TrackStore:
tracks: list[Track] = CustomList()
# {'trackhash': Track[]}
trackhashmap: dict[str, TrackGroup] = dict()
@classproperty
def tracks(cls) -> list[Track]:
return cls.get_flat_list()
@classmethod
def get_flat_list(cls):
"""
Returns a flat list of all tracks.
"""
return list(
itertools.chain.from_iterable(
[group.tracks for group in cls.trackhashmap.values()]
)
)
@classmethod
def load_all_tracks(cls, instance_key: str):
@@ -24,32 +105,60 @@ class TrackStore:
global TRACKS_LOAD_KEY
TRACKS_LOAD_KEY = instance_key
cls.tracks = CustomList(trackdb.get_all_tracks())
cls.trackhashmap = dict()
tracks = TrackTable.get_all()
favs = favdb.get_fav_tracks()
records = dict()
for fav in favs:
r = records.setdefault(fav[1], set())
r.add(fav[4])
for track in cls.tracks:
# INFO: Load all tracks into the dict store
for track in tracks:
if instance_key != TRACKS_LOAD_KEY:
return
userids = records.get(track.trackhash, set())
track.fav_userids = list(userids)
exists = cls.trackhashmap.get(track.trackhash, None)
if not exists:
cls.trackhashmap[track.trackhash] = TrackGroup([track])
else:
cls.trackhashmap[track.trackhash].append(track)
print("Done!")
# favs = favdb.get_fav_tracks()
favs = FavoritesTable.get_all()
records: dict[str, set[int]] = dict()
# convert records: {trackhash: {userid, userid, ...}}
for fav in favs:
if fav.hash not in records:
# if trackhash not in dict, add it
# and set the value to a set containing the userid
records[fav.hash] = {fav.userid}
# if trackhash is in dict, add the userid to the set
records[fav.hash].add(fav.userid)
for record in records:
if instance_key != TRACKS_LOAD_KEY:
return
group = cls.trackhashmap.get(record, None)
if not group:
continue
group.set_fav_userids(records.get(record, set()))
# print("Done!")
# print(cls.trackhashmap.get("0d6b22c19c").tracks[0].fav_userids)
# sys.exit(0)
@classmethod
def add_track(cls, track: Track):
"""
Adds a single track to the store.
"""
group = cls.trackhashmap.get(track.trackhash, None)
cls.tracks.append(track)
if group:
return group.append(track)
cls.trackhashmap[track.trackhash] = TrackGroup([track])
@classmethod
def add_tracks(cls, tracks: list[Track]):
@@ -57,17 +166,21 @@ class TrackStore:
Adds multiple tracks to the store.
"""
cls.tracks.extend(tracks)
for track in tracks:
cls.add_track(track)
@classmethod
def remove_track_obj(cls, track: Track):
def remove_track(cls, track: Track):
"""
Removes a single track from the store.
"""
try:
cls.tracks.remove(track)
except ValueError:
pass
group = cls.trackhashmap.get(track.trackhash, None)
if group:
group.remove(track)
if len(group) == 0:
del cls.trackhashmap[track.trackhash]
@classmethod
def remove_track_by_filepath(cls, filepath: str):
@@ -75,10 +188,7 @@ class TrackStore:
Removes a track from the store by its filepath.
"""
for track in cls.tracks:
if track.filepath == filepath:
cls.remove_track_obj(track)
break
return cls.remove_tracks_by_filepaths({filepath})
@classmethod
def remove_tracks_by_filepaths(cls, filepaths: set[str]):
@@ -86,47 +196,47 @@ class TrackStore:
Removes multiple tracks from the store by their filepaths.
"""
for track in cls.tracks:
if track.filepath in filepaths:
cls.remove_track_obj(track)
filecount = len(filepaths)
for trackhash in cls.trackhashmap:
group = cls.trackhashmap[trackhash]
for track in group.tracks:
if track.filepath in filepaths:
group.remove(track)
if len(group) == 0:
del cls.trackhashmap[trackhash]
filecount -= 1
if filecount == 0:
break
@classmethod
def count_tracks_by_trackhash(cls, trackhash: str) -> int:
"""
Counts the number of tracks with a specific trackhash.
"""
return sum(1 for track in cls.tracks if track.trackhash == trackhash)
return len(cls.trackhashmap.get(trackhash, []))
@classmethod
def make_track_fav(cls, trackhash: str):
def toggle_favorite(cls, trackhash: str, remove: bool = False):
"""
Adds a track to the favorites.
"""
for track in cls.tracks:
if track.trackhash == trackhash:
if current_user["id"] not in track.fav_userids:
track.fav_userids.append(current_user["id"])
group = cls.trackhashmap.get(trackhash)
if group:
group.toggle_favorite(remove=remove)
@classmethod
def remove_track_from_fav(cls, trackhash: str):
"""
Removes a track from the favorites.
"""
for track in cls.tracks:
if track.trackhash == trackhash:
if current_user["id"] in track.fav_userids:
track.fav_userids.remove(current_user["id"])
@classmethod
def append_track_artists(
cls, albumhash: str, artists: list[str], new_album_title: str
):
tracks = cls.get_tracks_by_albumhash(albumhash)
for track in tracks:
track.add_artists(artists, new_album_title)
return cls.toggle_favorite(trackhash, remove=True)
# ================================================
# ================== GETTERS =====================
@@ -138,16 +248,16 @@ class TrackStore:
Returns a list of tracks by their hashes.
"""
hash_set = set(trackhashes)
set_len = len(hash_set)
tracks = []
for track in cls.tracks:
if track.trackhash in hash_set:
tracks: list[Track] = []
for trackhash in hash_set:
group = cls.trackhashmap.get(trackhash, None)
if group:
track = group.get_best()
tracks.append(track)
if len(tracks) == set_len:
break
# sort the tracks in the order of the given trackhashes
tracks.sort(key=lambda t: trackhashes.index(t.trackhash))
return tracks
@@ -156,32 +266,86 @@ class TrackStore:
def get_tracks_by_filepaths(cls, paths: list[str]) -> list[Track]:
"""
Returns all tracks matching the given paths.
⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔⛔
"""
tracks = sorted(cls.tracks, key=lambda x: x.filepath)
tracks = use_bisection(tracks, "filepath", paths)
return [track for track in tracks if track is not None]
# tracks = sorted(cls.trackhashmap, key=lambda x: x.filepath)
# tracks = use_bisection(tracks, "filepath", paths)
# return [track for track in tracks if track is not None]
# return cls.find_tracks_by(key="filepath", value=paths)
tracks: list[Track] = []
for trackhash in cls.trackhashmap:
group = cls.trackhashmap.get(trackhash)
if not group:
continue
for track in group.tracks:
if track.filepath in paths:
tracks.append(track)
return tracks
@classmethod
def find_tracks_by(
cls,
key: str,
value: str,
predicate: Callable = lambda prop_value, value: prop_value == value,
including_duplicates: bool = False,
):
"""
Find all tracks by a specific key.
"""
tracks: list[Track] = []
for trackhash in cls.trackhashmap:
group = cls.trackhashmap.get(trackhash, None)
if not group:
continue
for track in group.tracks:
prop_value = getattr(track, key)
if predicate(prop_value, value):
tracks.append(track)
if including_duplicates:
return tracks
return remove_duplicates(tracks)
@classmethod
def get_tracks_by_albumhash(cls, album_hash: str) -> list[Track]:
"""
Returns all tracks matching the given album hash.
"""
tracks = [t for t in cls.tracks if t.albumhash == album_hash]
return remove_duplicates(tracks, is_album_tracks=True)
return cls.find_tracks_by(key="albumhash", value=album_hash)
@classmethod
def get_tracks_by_artisthash(cls, artisthash: str):
"""
Returns all tracks matching the given artist. Duplicate tracks are removed.
"""
tracks = [t for t in cls.tracks if artisthash in t.artist_hashes]
tracks = remove_duplicates(tracks)
tracks.sort(key=lambda x: x.last_mod)
return tracks
predicate = lambda artisthashes, artisthash: artisthash in artisthashes
return cls.find_tracks_by(
key="artist_hashes", value=artisthash, predicate=predicate
)
@classmethod
def get_tracks_in_path(cls, path: str):
"""
Returns all tracks in the given path.
"""
return (t for t in cls.tracks if t.folder.startswith(path))
predicate: Callable[[str, str], bool] = (
lambda track_folder, path: track_folder.startswith(path)
)
return cls.find_tracks_by(
key="folder",
value=path,
predicate=predicate,
including_duplicates=True,
)