store playcount and duration on the track table

+ allow sorting all items with those two
+ add methods to update scrobble info
This commit is contained in:
cwilvx
2024-06-30 19:33:13 +03:00
parent 4a9f804e70
commit b9ad07441a
15 changed files with 161 additions and 45 deletions
+3 -1
View File
@@ -44,4 +44,6 @@
- Move plugins to a config file - Move plugins to a config file
- What about our migrations? - What about our migrations?
- Add userid to queries - Add userid to queries
- Remove duplicates on artist page (test with Hanson) - Remove duplicates on artist page (test with Hanson)
- Test foreign keys on delete
- Map scrobble info on app start
+2 -2
View File
@@ -26,7 +26,7 @@ from app.api import (
settings, settings,
lyrics, lyrics,
plugins, plugins,
logger, scrobble,
home, home,
getall, getall,
auth, auth,
@@ -116,7 +116,7 @@ def create_api():
app.register_api(lyrics_plugin.api) app.register_api(lyrics_plugin.api)
# Logger # Logger
app.register_api(logger.api) app.register_api(scrobble.api)
# Home # Home
app.register_api(home.api) app.register_api(home.api)
+25 -1
View File
@@ -18,6 +18,7 @@ from app.utils.dates import (
create_new_date, create_new_date,
date_string_to_time_passed, date_string_to_time_passed,
seconds_to_time_string, seconds_to_time_string,
timestamp_to_time_passed,
) )
bp_tag = Tag(name="Get all", description="List all items") bp_tag = Tag(name="Get all", description="List all items")
@@ -57,6 +58,13 @@ def get_all_items(path: GetAllItemsPath, query: GetAllItemsQuery):
Get all items Get all items
Used to show all albums or artists in the library Used to show all albums or artists in the library
Sort keys:
-
Both albums and artists: `duration`, `created_date`, `playcount`, `playduration`, `lastplayed`, `trackcount`
Albums only: `title`, `albumartists`, `date`
Artists only: `name`, `albumcount`
""" """
is_albums = path.itemtype == "albums" is_albums = path.itemtype == "albums"
is_artists = path.itemtype == "artists" is_artists = path.itemtype == "artists"
@@ -76,6 +84,9 @@ def get_all_items(path: GetAllItemsPath, query: GetAllItemsQuery):
sort_is_count = sort == "trackcount" sort_is_count = sort == "trackcount"
sort_is_duration = sort == "duration" sort_is_duration = sort == "duration"
sort_is_create_date = sort == "created_date" sort_is_create_date = sort == "created_date"
sort_is_playcount = sort == "playcount"
sort_is_playduration = sort == "playduration"
sort_is_lastplayed = sort == "lastplayed"
sort_is_date = is_albums and sort == "date" sort_is_date = is_albums and sort == "date"
sort_is_artist = is_albums and sort == "albumartists" sort_is_artist = is_albums and sort == "albumartists"
@@ -94,7 +105,6 @@ def get_all_items(path: GetAllItemsPath, query: GetAllItemsQuery):
for item in items: for item in items:
item_dict = serialize_album(item) if is_albums else serialize_artist(item) item_dict = serialize_album(item) if is_albums else serialize_artist(item)
print(item_dict)
if sort_is_date: if sort_is_date:
item_dict["help_text"] = item.date item_dict["help_text"] = item.date
@@ -122,6 +132,20 @@ def get_all_items(path: GetAllItemsPath, query: GetAllItemsQuery):
f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}" f"{format_number(item.albumcount)} album{'' if item.albumcount == 1 else 's'}"
) )
if sort_is_playcount:
item_dict["help_text"] = (
f"{format_number(item.playcount)} play{'' if item.playcount == 1 else 's'}"
)
if sort_is_lastplayed:
if item.playduration == 0:
item_dict["help_text"] = "Never played"
else:
item_dict["help_text"] = timestamp_to_time_passed(item.lastplayed)
if sort_is_playduration:
item_dict["help_text"] = seconds_to_time_string(item.playduration)
album_list.append(item_dict) album_list.append(item_dict)
return {"items": album_list, "total": total} return {"items": album_list, "total": total}
@@ -3,7 +3,8 @@ from flask_openapi3 import APIBlueprint
from pydantic import Field from pydantic import Field
from app.api.apischemas import TrackHashSchema from app.api.apischemas import TrackHashSchema
from app.db.sqlite.logger.tracks import SQLiteTrackLogger as db from app.db.libdata import AlbumTable, ArtistTable, TrackTable
from app.db.userdata import ScrobbleTable
from app.settings import Defaults from app.settings import Defaults
bp_tag = Tag(name="Logger", description="Log item plays") bp_tag = Tag(name="Logger", description="Log item plays")
@@ -26,19 +27,20 @@ def log_track(body: LogTrackBody):
""" """
Log a track play to the database. Log a track play to the database.
""" """
trackhash = body.trackhash
timestamp = body.timestamp timestamp = body.timestamp
duration = body.duration duration = body.duration
source = body.source
if not timestamp or duration < 5: if not timestamp or duration < 5:
return {"msg": "Invalid entry."}, 400 return {"msg": "Invalid entry."}, 400
last_row = db.insert_track( track = TrackTable.get_track_by_trackhash(body.trackhash)
trackhash=trackhash,
timestamp=timestamp,
duration=duration,
source=source,
)
return {"total entries": last_row} if track is None:
return {"msg": "Track not found."}, 404
ScrobbleTable.add(dict(body))
TrackTable.increment_playcount(body.trackhash, duration, timestamp)
AlbumTable.increment_playcount(track.albumhash, duration, timestamp)
ArtistTable.increment_playcount(track.artisthashes, duration, timestamp)
return {"msg": "recorded"}, 201
+1 -1
View File
@@ -48,7 +48,7 @@ class DbManager:
if self.commit: if self.commit:
self.conn.commit() self.conn.commit()
self.conn.close() # self.conn.close()
class Base(MappedAsDataclass, DeclarativeBase): class Base(MappedAsDataclass, DeclarativeBase):
+60 -4
View File
@@ -73,6 +73,21 @@ class Base(MasterBase, DeclarativeBase):
conn.execute(stmt) conn.execute(stmt)
@classmethod
def increment_scrobblecount(
cls, table: Any, field: Any, hash: str, duration: int, timestamp: int
):
cls.execute(
update(table)
.where(field == hash)
.values(
playcount=table.playcount + 1,
playduration=table.playduration + duration,
lastplayed=timestamp,
),
commit=True,
)
class TrackTable(Base): class TrackTable(Base):
__tablename__ = "track" __tablename__ = "track"
@@ -99,8 +114,12 @@ class TrackTable(Base):
track: Mapped[int] = mapped_column(Integer()) track: Mapped[int] = mapped_column(Integer())
trackhash: Mapped[str] = mapped_column(String(), index=True) trackhash: Mapped[str] = mapped_column(String(), index=True)
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean()) is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
playcount: Mapped[int] = mapped_column(Integer()) lastplayed: Mapped[int] = mapped_column(Integer(), default=0)
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON()) playcount: Mapped[int] = mapped_column(Integer(), default=0)
playduration: Mapped[int] = mapped_column(Integer(), default=0)
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(
JSON(), default_factory=dict
)
@classmethod @classmethod
def get_all(cls): def get_all(cls):
@@ -180,6 +199,12 @@ class TrackTable(Base):
with DbManager(commit=True) as conn: with DbManager(commit=True) as conn:
conn.execute(delete(TrackTable).where(TrackTable.filepath.in_(filepaths))) conn.execute(delete(TrackTable).where(TrackTable.filepath.in_(filepaths)))
@classmethod
def increment_playcount(cls, trackhash: str, duration: int, timestamp: int):
cls.increment_scrobblecount(
TrackTable, TrackTable.trackhash, trackhash, duration, timestamp
)
class AlbumTable(Base): class AlbumTable(Base):
__tablename__ = "album" __tablename__ = "album"
@@ -199,7 +224,12 @@ class AlbumTable(Base):
title: Mapped[str] = mapped_column(String()) title: Mapped[str] = mapped_column(String())
trackcount: Mapped[int] = mapped_column(Integer()) trackcount: Mapped[int] = mapped_column(Integer())
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean()) is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON()) lastplayed: Mapped[int] = mapped_column(Integer(), default=0)
playcount: Mapped[int] = mapped_column(Integer(), default=0)
playduration: Mapped[int] = mapped_column(Integer(), default=0)
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(
JSON(), default_factory=dict
)
@classmethod @classmethod
def get_all(cls): def get_all(cls):
@@ -257,6 +287,12 @@ class AlbumTable(Base):
) )
return albums_to_dataclasses(result.all()) return albums_to_dataclasses(result.all())
@classmethod
def increment_playcount(cls, albumhash: str, duration: int, timestamp: int):
return cls.increment_scrobblecount(
AlbumTable, AlbumTable.albumhash, albumhash, duration, timestamp
)
class ArtistTable(Base): class ArtistTable(Base):
__tablename__ = "artist" __tablename__ = "artist"
@@ -272,7 +308,12 @@ class ArtistTable(Base):
name: Mapped[str] = mapped_column(String(), index=True) name: Mapped[str] = mapped_column(String(), index=True)
trackcount: Mapped[int] = mapped_column(Integer()) trackcount: Mapped[int] = mapped_column(Integer())
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean()) is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON()) lastplayed: Mapped[int] = mapped_column(Integer(), default=0)
playcount: Mapped[int] = mapped_column(Integer(), default=0)
playduration: Mapped[int] = mapped_column(Integer(), default=0)
extra: Mapped[Optional[dict[str, Any]]] = mapped_column(
JSON(), default_factory=dict
)
@classmethod @classmethod
def get_all(cls): def get_all(cls):
@@ -310,3 +351,18 @@ class ArtistTable(Base):
.limit(limit) .limit(limit)
) )
return artists_to_dataclasses(result.fetchall()) return artists_to_dataclasses(result.fetchall())
@classmethod
def increment_playcount(
cls, artisthashes: list[str], duration: int, timestamp: int
):
cls.execute(
update(cls)
.where(ArtistTable.artisthash.in_(artisthashes))
.values(
playcount=ArtistTable.playcount + 1,
playduration=ArtistTable.playduration + duration,
lastplayed=timestamp,
),
commit=True,
)
+25 -4
View File
@@ -31,7 +31,7 @@ from app.db.utils import (
) )
from app.db import Base, DbManager from app.db import Base, DbManager
from app.utils.auth import hash_password from app.utils.auth import get_current_userid, hash_password
class UserTable(Base): class UserTable(Base):
@@ -160,7 +160,7 @@ class FavoritesTable(Base):
type: Mapped[str] = mapped_column(String(), index=True) type: Mapped[str] = mapped_column(String(), index=True)
timestamp: Mapped[int] = mapped_column(Integer(), index=True) timestamp: Mapped[int] = mapped_column(Integer(), index=True)
userid: Mapped[int] = mapped_column( userid: Mapped[int] = mapped_column(
Integer(), ForeignKey("user.id"), default=1, index=True Integer(), ForeignKey("user.id", ondelete="cascade"), default=1, index=True
) )
extra: Mapped[dict[str, Any]] = mapped_column( extra: Mapped[dict[str, Any]] = mapped_column(
JSON(), nullable=True, default_factory=dict JSON(), nullable=True, default_factory=dict
@@ -175,7 +175,7 @@ class FavoritesTable(Base):
@classmethod @classmethod
def insert_item(cls, item: dict[str, Any]): def insert_item(cls, item: dict[str, Any]):
item["timestamp"] = int(datetime.datetime.now().timestamp()) item["timestamp"] = int(datetime.datetime.now().timestamp())
item["userid"] = current_user["id"] item["userid"] = get_current_userid()
with DbManager(commit=True) as conn: with DbManager(commit=True) as conn:
conn.execute(insert(cls).values(item)) conn.execute(insert(cls).values(item))
@@ -199,7 +199,7 @@ class FavoritesTable(Base):
result = cls.execute( result = cls.execute(
select(table) select(table)
.select_from(join(table, cls, field == cls.hash)) .select_from(join(table, cls, field == cls.hash))
.where(and_(cls.type == type, cls.userid == current_user["id"])) .where(and_(cls.type == type, cls.userid == get_current_userid()))
.offset(start) .offset(start)
# INFO: If start is 0, fetch all so we can get the total count # INFO: If start is 0, fetch all so we can get the total count
.limit(limit if start != 0 else None) .limit(limit if start != 0 else None)
@@ -238,3 +238,24 @@ class FavoritesTable(Base):
ArtistTable, ArtistTable.artisthash, "artist", start, limit ArtistTable, ArtistTable.artisthash, "artist", start, limit
) )
return artists_to_dataclasses(result), total return artists_to_dataclasses(result), total
class ScrobbleTable(Base):
__tablename__ = "scrobble"
id: Mapped[int] = mapped_column(primary_key=True)
trackhash: Mapped[str] = mapped_column(String(), index=True)
duration: Mapped[int] = mapped_column(Integer())
timestamp: Mapped[int] = mapped_column(Integer())
source: Mapped[str] = mapped_column(String())
userid: Mapped[int] = mapped_column(
Integer(), ForeignKey("user.id", ondelete="cascade"), index=True
)
extra: Mapped[dict[str, Any]] = mapped_column(
JSON(), nullable=True, default_factory=dict
)
@classmethod
def add(cls, item: dict[str, Any]):
item["userid"] = get_current_userid()
return cls.insert_one(item)
+20 -21
View File
@@ -156,32 +156,32 @@ class IndexAlbums:
"albumhash": track.albumhash, "albumhash": track.albumhash,
"base_title": None, "base_title": None,
"color": None, "color": None,
"created_date": None, "created_date": track.last_mod,
"date": None, "date": track.date,
"duration": track.duration, "duration": track.duration,
"genres": [*track.genres] if track.genres else [], "genres": [*track.genres] if track.genres else [],
"og_title": track.og_album, "og_title": track.og_album,
"lastplayed": track.lastplayed,
"playcount": track.playcount,
"playduration": track.playduration,
"title": track.album, "title": track.album,
"trackcount": 1, "trackcount": 1,
"dates": [track.date],
"created_dates": [track.last_mod],
} }
else: else:
album = albums[track.albumhash] album = albums[track.albumhash]
album["trackcount"] += 1 album["trackcount"] += 1
album["playcount"] += track.playcount
album["playduration"] += track.playduration
album["lastplayed"] = max(album["lastplayed"], track.lastplayed)
album["duration"] += track.duration album["duration"] += track.duration
album["dates"].append(track.date) album["date"] = min(album["date"], track.date)
album["created_dates"].append(track.last_mod) album["created_date"] = min(album["created_date"], track.last_mod)
if track.genres: if track.genres:
album["genres"].extend(track.genres) album["genres"].extend(track.genres)
for album in albums.values(): for album in albums.values():
album["date"] = min(album["dates"])
album["created_date"] = min(album["created_dates"])
genres = [] genres = []
for genre in album["genres"]: for genre in album["genres"]:
if genre not in genres: if genre not in genres:
genres.append(genre) genres.append(genre)
@@ -190,8 +190,6 @@ class IndexAlbums:
album["base_title"], _ = get_base_album_title(album["og_title"]) album["base_title"], _ = get_base_album_title(album["og_title"])
del genres del genres
del album["dates"]
del album["created_dates"]
AlbumTable.remove_all() AlbumTable.remove_all()
AlbumTable.insert_many(list(albums.values())) AlbumTable.insert_many(list(albums.values()))
@@ -219,23 +217,28 @@ class IndexArtists:
"albumcount": None, "albumcount": None,
"albums": {track.albumhash}, "albums": {track.albumhash},
"artisthash": thisartist["artisthash"], "artisthash": thisartist["artisthash"],
"created_dates": [track.last_mod], "created_date": track.last_mod,
"dates": [track.date], "date": track.date,
"date": None,
"duration": track.duration, "duration": track.duration,
"genres": track.genres if track.genres else [], "genres": track.genres if track.genres else [],
"name": None, "name": None,
"names": {thisartist["name"]}, "names": {thisartist["name"]},
"lastplayed": track.lastplayed,
"playcount": track.playcount,
"playduration": track.playduration,
"trackcount": None, "trackcount": None,
"tracks": {track.trackhash}, "tracks": {track.trackhash},
} }
else: else:
artist = artists[thisartist["artisthash"]] artist = artists[thisartist["artisthash"]]
artist["duration"] += track.duration artist["duration"] += track.duration
artist["playcount"] += track.playcount
artist["playduration"] += track.playduration
artist["albums"].add(track.albumhash) artist["albums"].add(track.albumhash)
artist["tracks"].add(track.trackhash) artist["tracks"].add(track.trackhash)
artist["dates"].append(track.date) artist["date"] = min(artist["date"], track.date)
artist["created_dates"].append(track.last_mod) artist["lastplayed"] = max(artist["lastplayed"], track.lastplayed)
artist["created_date"] = min(artist["created_date"], track.last_mod)
artist["names"].add(thisartist["name"]) artist["names"].add(thisartist["name"])
if track.genres: if track.genres:
@@ -244,8 +247,6 @@ class IndexArtists:
for artist in artists.values(): for artist in artists.values():
artist["albumcount"] = len(artist["albums"]) artist["albumcount"] = len(artist["albums"])
artist["trackcount"] = len(artist["tracks"]) artist["trackcount"] = len(artist["tracks"])
artist["date"] = min(artist["dates"])
artist["created_date"] = min(artist["created_dates"])
genres = [] genres = []
@@ -260,8 +261,6 @@ class IndexArtists:
del artist["names"] del artist["names"]
del artist["tracks"] del artist["tracks"]
del artist["albums"] del artist["albums"]
del artist["dates"]
del artist["created_dates"]
# INFO: Delete local variables # INFO: Delete local variables
del genres del genres
+3
View File
@@ -29,6 +29,9 @@ class Album:
title: str title: str
trackcount: int trackcount: int
is_favorite: bool is_favorite: bool
lastplayed: int
playcount: int
playduration: int
extra: dict extra: dict
type: str = "album" type: str = "album"
+3
View File
@@ -48,6 +48,9 @@ class Artist:
name: str name: str
trackcount: int trackcount: int
is_favorite: bool is_favorite: bool
lastplayed: int
playcount: int
playduration: int
extra: dict extra: dict
image: str = "" image: str = ""
+3
View File
@@ -29,6 +29,9 @@ class Track:
track: int track: int
trackhash: str trackhash: str
extra: dict extra: dict
lastplayed: int
playcount: int
playduration: int
is_favorite: bool = False is_favorite: bool = False
_pos: int = 0 _pos: int = 0
+1
View File
@@ -27,6 +27,7 @@ def serialize_for_card(album: Album):
"og_title", "og_title",
"base_title", "base_title",
"genres", "genres",
"playcount"
} }
return album_serializer(album, props_to_remove) return album_serializer(album, props_to_remove)
+1
View File
@@ -14,6 +14,7 @@ def serialize_for_card(artist: Artist):
"trackcount", "trackcount",
"duration", "duration",
"albumcount", "albumcount",
"playcount",
} }
for key in props_to_remove: for key in props_to_remove:
+1
View File
@@ -20,6 +20,7 @@ def serialize_track(track: Track, to_remove: set = {}, remove_disc=True) -> dict
"artist_hashes", "artist_hashes",
"created_date", "created_date",
"fav_userids", "fav_userids",
"playcount",
}.union(to_remove) }.union(to_remove)
if not remove_disc: if not remove_disc:
+1 -1
View File
@@ -63,4 +63,4 @@ def seconds_to_time_string(seconds):
if minutes > 0: if minutes > 0:
return f"{minutes} minute{'s' if minutes > 1 else ''}" return f"{minutes} minute{'s' if minutes > 1 else ''}"
return f"{remaining_seconds} second{'s' if remaining_seconds > 1 else ''}" return f"{remaining_seconds} second{'' if remaining_seconds == 1 else 's'}"