mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -48,7 +48,7 @@ class DbManager:
|
||||
if self.commit:
|
||||
self.conn.commit()
|
||||
|
||||
self.conn.close()
|
||||
# self.conn.close()
|
||||
|
||||
|
||||
class Base(MappedAsDataclass, DeclarativeBase):
|
||||
|
||||
+60
-4
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -29,6 +29,9 @@ class Album:
|
||||
title: str
|
||||
trackcount: int
|
||||
is_favorite: bool
|
||||
lastplayed: int
|
||||
playcount: int
|
||||
playduration: int
|
||||
extra: dict
|
||||
|
||||
type: str = "album"
|
||||
|
||||
@@ -48,6 +48,9 @@ class Artist:
|
||||
name: str
|
||||
trackcount: int
|
||||
is_favorite: bool
|
||||
lastplayed: int
|
||||
playcount: int
|
||||
playduration: int
|
||||
extra: dict
|
||||
|
||||
image: str = ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,6 +27,7 @@ def serialize_for_card(album: Album):
|
||||
"og_title",
|
||||
"base_title",
|
||||
"genres",
|
||||
"playcount"
|
||||
}
|
||||
|
||||
return album_serializer(album, props_to_remove)
|
||||
|
||||
@@ -14,6 +14,7 @@ def serialize_for_card(artist: Artist):
|
||||
"trackcount",
|
||||
"duration",
|
||||
"albumcount",
|
||||
"playcount",
|
||||
}
|
||||
|
||||
for key in props_to_remove:
|
||||
|
||||
@@ -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
@@ -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'}"
|
||||
|
||||
Reference in New Issue
Block a user