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
+2
View File
@@ -45,3 +45,5 @@
- What about our migrations?
- Add userid to queries
- 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,
lyrics,
plugins,
logger,
scrobble,
home,
getall,
auth,
@@ -116,7 +116,7 @@ def create_api():
app.register_api(lyrics_plugin.api)
# Logger
app.register_api(logger.api)
app.register_api(scrobble.api)
# Home
app.register_api(home.api)
+25 -1
View File
@@ -18,6 +18,7 @@ from app.utils.dates import (
create_new_date,
date_string_to_time_passed,
seconds_to_time_string,
timestamp_to_time_passed,
)
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
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_artists = path.itemtype == "artists"
@@ -76,6 +84,9 @@ def get_all_items(path: GetAllItemsPath, query: GetAllItemsQuery):
sort_is_count = sort == "trackcount"
sort_is_duration = sort == "duration"
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_artist = is_albums and sort == "albumartists"
@@ -94,7 +105,6 @@ def get_all_items(path: GetAllItemsPath, query: GetAllItemsQuery):
for item in items:
item_dict = serialize_album(item) if is_albums else serialize_artist(item)
print(item_dict)
if sort_is_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'}"
)
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)
return {"items": album_list, "total": total}
@@ -3,7 +3,8 @@ from flask_openapi3 import APIBlueprint
from pydantic import Field
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
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.
"""
trackhash = body.trackhash
timestamp = body.timestamp
duration = body.duration
source = body.source
if not timestamp or duration < 5:
return {"msg": "Invalid entry."}, 400
last_row = db.insert_track(
trackhash=trackhash,
timestamp=timestamp,
duration=duration,
source=source,
)
track = TrackTable.get_track_by_trackhash(body.trackhash)
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:
self.conn.commit()
self.conn.close()
# self.conn.close()
class Base(MappedAsDataclass, DeclarativeBase):
+60 -4
View File
@@ -73,6 +73,21 @@ class Base(MasterBase, DeclarativeBase):
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):
__tablename__ = "track"
@@ -99,8 +114,12 @@ class TrackTable(Base):
track: Mapped[int] = mapped_column(Integer())
trackhash: Mapped[str] = mapped_column(String(), index=True)
is_favorite: Mapped[Optional[bool]] = mapped_column(Boolean())
playcount: Mapped[int] = mapped_column(Integer())
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
def get_all(cls):
@@ -180,6 +199,12 @@ class TrackTable(Base):
with DbManager(commit=True) as conn:
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):
__tablename__ = "album"
@@ -199,7 +224,12 @@ class AlbumTable(Base):
title: Mapped[str] = mapped_column(String())
trackcount: Mapped[int] = mapped_column(Integer())
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
def get_all(cls):
@@ -257,6 +287,12 @@ class AlbumTable(Base):
)
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):
__tablename__ = "artist"
@@ -272,7 +308,12 @@ class ArtistTable(Base):
name: Mapped[str] = mapped_column(String(), index=True)
trackcount: Mapped[int] = mapped_column(Integer())
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
def get_all(cls):
@@ -310,3 +351,18 @@ class ArtistTable(Base):
.limit(limit)
)
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.utils.auth import hash_password
from app.utils.auth import get_current_userid, hash_password
class UserTable(Base):
@@ -160,7 +160,7 @@ class FavoritesTable(Base):
type: Mapped[str] = mapped_column(String(), index=True)
timestamp: Mapped[int] = mapped_column(Integer(), index=True)
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(
JSON(), nullable=True, default_factory=dict
@@ -175,7 +175,7 @@ class FavoritesTable(Base):
@classmethod
def insert_item(cls, item: dict[str, Any]):
item["timestamp"] = int(datetime.datetime.now().timestamp())
item["userid"] = current_user["id"]
item["userid"] = get_current_userid()
with DbManager(commit=True) as conn:
conn.execute(insert(cls).values(item))
@@ -199,7 +199,7 @@ class FavoritesTable(Base):
result = cls.execute(
select(table)
.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)
# INFO: If start is 0, fetch all so we can get the total count
.limit(limit if start != 0 else None)
@@ -238,3 +238,24 @@ class FavoritesTable(Base):
ArtistTable, ArtistTable.artisthash, "artist", start, limit
)
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,
"base_title": None,
"color": None,
"created_date": None,
"date": None,
"created_date": track.last_mod,
"date": track.date,
"duration": track.duration,
"genres": [*track.genres] if track.genres else [],
"og_title": track.og_album,
"lastplayed": track.lastplayed,
"playcount": track.playcount,
"playduration": track.playduration,
"title": track.album,
"trackcount": 1,
"dates": [track.date],
"created_dates": [track.last_mod],
}
else:
album = albums[track.albumhash]
album["trackcount"] += 1
album["playcount"] += track.playcount
album["playduration"] += track.playduration
album["lastplayed"] = max(album["lastplayed"], track.lastplayed)
album["duration"] += track.duration
album["dates"].append(track.date)
album["created_dates"].append(track.last_mod)
album["date"] = min(album["date"], track.date)
album["created_date"] = min(album["created_date"], track.last_mod)
if track.genres:
album["genres"].extend(track.genres)
for album in albums.values():
album["date"] = min(album["dates"])
album["created_date"] = min(album["created_dates"])
genres = []
for genre in album["genres"]:
if genre not in genres:
genres.append(genre)
@@ -190,8 +190,6 @@ class IndexAlbums:
album["base_title"], _ = get_base_album_title(album["og_title"])
del genres
del album["dates"]
del album["created_dates"]
AlbumTable.remove_all()
AlbumTable.insert_many(list(albums.values()))
@@ -219,23 +217,28 @@ class IndexArtists:
"albumcount": None,
"albums": {track.albumhash},
"artisthash": thisartist["artisthash"],
"created_dates": [track.last_mod],
"dates": [track.date],
"date": None,
"created_date": track.last_mod,
"date": track.date,
"duration": track.duration,
"genres": track.genres if track.genres else [],
"name": None,
"names": {thisartist["name"]},
"lastplayed": track.lastplayed,
"playcount": track.playcount,
"playduration": track.playduration,
"trackcount": None,
"tracks": {track.trackhash},
}
else:
artist = artists[thisartist["artisthash"]]
artist["duration"] += track.duration
artist["playcount"] += track.playcount
artist["playduration"] += track.playduration
artist["albums"].add(track.albumhash)
artist["tracks"].add(track.trackhash)
artist["dates"].append(track.date)
artist["created_dates"].append(track.last_mod)
artist["date"] = min(artist["date"], track.date)
artist["lastplayed"] = max(artist["lastplayed"], track.lastplayed)
artist["created_date"] = min(artist["created_date"], track.last_mod)
artist["names"].add(thisartist["name"])
if track.genres:
@@ -244,8 +247,6 @@ class IndexArtists:
for artist in artists.values():
artist["albumcount"] = len(artist["albums"])
artist["trackcount"] = len(artist["tracks"])
artist["date"] = min(artist["dates"])
artist["created_date"] = min(artist["created_dates"])
genres = []
@@ -260,8 +261,6 @@ class IndexArtists:
del artist["names"]
del artist["tracks"]
del artist["albums"]
del artist["dates"]
del artist["created_dates"]
# INFO: Delete local variables
del genres
+3
View File
@@ -29,6 +29,9 @@ class Album:
title: str
trackcount: int
is_favorite: bool
lastplayed: int
playcount: int
playduration: int
extra: dict
type: str = "album"
+3
View File
@@ -48,6 +48,9 @@ class Artist:
name: str
trackcount: int
is_favorite: bool
lastplayed: int
playcount: int
playduration: int
extra: dict
image: str = ""
+3
View File
@@ -29,6 +29,9 @@ class Track:
track: int
trackhash: str
extra: dict
lastplayed: int
playcount: int
playduration: int
is_favorite: bool = False
_pos: int = 0
+1
View File
@@ -27,6 +27,7 @@ def serialize_for_card(album: Album):
"og_title",
"base_title",
"genres",
"playcount"
}
return album_serializer(album, props_to_remove)
+1
View File
@@ -14,6 +14,7 @@ def serialize_for_card(artist: Artist):
"trackcount",
"duration",
"albumcount",
"playcount",
}
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",
"created_date",
"fav_userids",
"playcount",
}.union(to_remove)
if not remove_disc:
+1 -1
View File
@@ -63,4 +63,4 @@ def seconds_to_time_string(seconds):
if minutes > 0:
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'}"