mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
feat: use thumbnails from folders
+ cache failed lastfm scrobbles + implement lastfm scrobble filter + change /home to /nothome
This commit is contained in:
@@ -8,7 +8,7 @@ from app.lib.home.get_recently_played import get_recently_played
|
|||||||
from app.store.homepage import HomepageStore
|
from app.store.homepage import HomepageStore
|
||||||
|
|
||||||
bp_tag = Tag(name="Home", description="Homepage items")
|
bp_tag = Tag(name="Home", description="Homepage items")
|
||||||
api = APIBlueprint("home", __name__, url_prefix="/home", abp_tags=[bp_tag])
|
api = APIBlueprint("home", __name__, url_prefix="/nothome", abp_tags=[bp_tag])
|
||||||
|
|
||||||
|
|
||||||
@api.get("/recents/added")
|
@api.get("/recents/added")
|
||||||
|
|||||||
+108
-9
@@ -1,3 +1,4 @@
|
|||||||
|
from fileinput import filename
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask_openapi3 import Tag
|
from flask_openapi3 import Tag
|
||||||
from flask_openapi3 import APIBlueprint
|
from flask_openapi3 import APIBlueprint
|
||||||
@@ -5,6 +6,10 @@ from pydantic import BaseModel, Field
|
|||||||
from flask import send_from_directory
|
from flask import send_from_directory
|
||||||
|
|
||||||
from app.settings import Defaults, Paths
|
from app.settings import Defaults, Paths
|
||||||
|
from app.store.albums import AlbumStore
|
||||||
|
from app.store.tracks import TrackStore
|
||||||
|
from app.utils.threading import background
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
bp_tag = Tag(
|
bp_tag = Tag(
|
||||||
name="Images", description="Image filenames are constructured as '{itemhash}.webp'"
|
name="Images", description="Image filenames are constructured as '{itemhash}.webp'"
|
||||||
@@ -12,6 +17,74 @@ bp_tag = Tag(
|
|||||||
api = APIBlueprint("imgserver", __name__, url_prefix="/img", abp_tags=[bp_tag])
|
api = APIBlueprint("imgserver", __name__, url_prefix="/img", abp_tags=[bp_tag])
|
||||||
|
|
||||||
|
|
||||||
|
@background
|
||||||
|
def cache_thumbnails(filepath: Path, trackhash: str):
|
||||||
|
"""
|
||||||
|
Resizes the image and stores it in the cache directory.
|
||||||
|
"""
|
||||||
|
image = Image.open(filepath)
|
||||||
|
path = Path(Paths.get_image_cache_path())
|
||||||
|
aspect_ratio = image.width / image.height
|
||||||
|
|
||||||
|
sizes = {
|
||||||
|
"xsmall": 64,
|
||||||
|
"small": 96,
|
||||||
|
"medium": 256,
|
||||||
|
"large": 512,
|
||||||
|
}
|
||||||
|
|
||||||
|
for size, width in sizes.items():
|
||||||
|
width = min(width, image.width)
|
||||||
|
height = int(width / aspect_ratio)
|
||||||
|
|
||||||
|
resized_path = path / size / (trackhash + ".webp")
|
||||||
|
resized_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
image.resize((width, height)).save(resized_path, format="webp")
|
||||||
|
|
||||||
|
|
||||||
|
def find_thumbnail(albumhash: str, pathhash: str):
|
||||||
|
# entry = TrackStore.trackhashmap.get(albumhash)
|
||||||
|
entry = AlbumStore.albummap.get(albumhash)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
return None, None, ""
|
||||||
|
|
||||||
|
track_file = None
|
||||||
|
|
||||||
|
tracks = TrackStore.get_tracks_by_trackhashes(entry.trackhashes)
|
||||||
|
for track in tracks:
|
||||||
|
if track.pathhash == pathhash:
|
||||||
|
track_file = track
|
||||||
|
break
|
||||||
|
|
||||||
|
if track_file is None:
|
||||||
|
return None, None, ""
|
||||||
|
|
||||||
|
folder = Path(track_file.folder)
|
||||||
|
|
||||||
|
# INFO: Check if the folder has image files
|
||||||
|
extensions = [".jpg", ".jpeg", ".png", ".webp"]
|
||||||
|
hierarchy = ["cover", "front", "back", "folder", "album", "artwork"]
|
||||||
|
|
||||||
|
images: list[Path] = []
|
||||||
|
for item in folder.iterdir():
|
||||||
|
if item.suffix in extensions:
|
||||||
|
images.append(item)
|
||||||
|
|
||||||
|
if len(images) == 0:
|
||||||
|
return None, None, ""
|
||||||
|
|
||||||
|
# INFO: Check if the folder has image files in the hierarchy
|
||||||
|
for item in hierarchy:
|
||||||
|
for image in images:
|
||||||
|
if image.name.lower().startswith(item.lower()):
|
||||||
|
return image.parent, image.name, track_file.albumhash
|
||||||
|
|
||||||
|
# INFO: If no image falls in the hierarchy, return the first image
|
||||||
|
first_image = images[0]
|
||||||
|
return first_image.parent, first_image.name, track_file.albumhash
|
||||||
|
|
||||||
|
|
||||||
def send_fallback_img(filename: str = "default.webp"):
|
def send_fallback_img(filename: str = "default.webp"):
|
||||||
"""
|
"""
|
||||||
Returns the fallback image from the assets folder.
|
Returns the fallback image from the assets folder.
|
||||||
@@ -25,7 +98,9 @@ def send_fallback_img(filename: str = "default.webp"):
|
|||||||
return send_from_directory(folder, filename)
|
return send_from_directory(folder, filename)
|
||||||
|
|
||||||
|
|
||||||
def send_file_or_fallback(folder: str, filename: str, fallback: str = "default.webp"):
|
def send_file_or_fallback(
|
||||||
|
folder: str, filename: str, fallback: str = "default.webp", pathhash: str = ""
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Returns the file from the folder or the fallback image.
|
Returns the file from the folder or the fallback image.
|
||||||
"""
|
"""
|
||||||
@@ -34,6 +109,22 @@ def send_file_or_fallback(folder: str, filename: str, fallback: str = "default.w
|
|||||||
if fpath.exists():
|
if fpath.exists():
|
||||||
return send_from_directory(folder, filename)
|
return send_from_directory(folder, filename)
|
||||||
|
|
||||||
|
if pathhash != "":
|
||||||
|
# INFO: Check if the image is in the cache
|
||||||
|
cache_path = Path(Paths.get_image_cache_path()) / fpath.parent.name / filename
|
||||||
|
if cache_path.exists():
|
||||||
|
return send_from_directory(cache_path.parent, cache_path.name)
|
||||||
|
|
||||||
|
# INFO: Find the thumbnail
|
||||||
|
parent, file, albumhash = find_thumbnail(
|
||||||
|
filename.replace(".webp", ""), pathhash
|
||||||
|
)
|
||||||
|
|
||||||
|
# INFO: Cache and send the thumbnail
|
||||||
|
if file is not None and parent is not None:
|
||||||
|
cache_thumbnails(parent / file, albumhash)
|
||||||
|
return send_from_directory(parent, file)
|
||||||
|
|
||||||
return send_fallback_img(fallback)
|
return send_fallback_img(fallback)
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +135,13 @@ class ImagePath(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageQuery(BaseModel):
|
||||||
|
pathhash: str = Field(
|
||||||
|
description="The path hash used to find the thumbnail",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# @api.get("/t/o/<imgpath>")
|
# @api.get("/t/o/<imgpath>")
|
||||||
# def send_original_thumbnail(path: ImagePath):
|
# def send_original_thumbnail(path: ImagePath):
|
||||||
# """
|
# """
|
||||||
@@ -60,39 +158,39 @@ class ImagePath(BaseModel):
|
|||||||
|
|
||||||
# TRACK THUMBNAILS
|
# TRACK THUMBNAILS
|
||||||
@api.get("/thumbnail/<imgpath>")
|
@api.get("/thumbnail/<imgpath>")
|
||||||
def send_lg_thumbnail(path: ImagePath):
|
def send_lg_thumbnail(path: ImagePath, query: ImageQuery):
|
||||||
"""
|
"""
|
||||||
Get large thumbnail (500 x 500)
|
Get large thumbnail (500 x 500)
|
||||||
"""
|
"""
|
||||||
folder = Paths.get_lg_thumb_path()
|
folder = Paths.get_lg_thumb_path()
|
||||||
return send_file_or_fallback(folder, path.imgpath)
|
return send_file_or_fallback(folder, path.imgpath, pathhash=query.pathhash)
|
||||||
|
|
||||||
|
|
||||||
@api.get("/thumbnail/xsmall/<imgpath>")
|
@api.get("/thumbnail/xsmall/<imgpath>")
|
||||||
def send_xsm_thumbnail(path: ImagePath):
|
def send_xsm_thumbnail(path: ImagePath, query: ImageQuery):
|
||||||
"""
|
"""
|
||||||
Get extra small thumbnail (64px)
|
Get extra small thumbnail (64px)
|
||||||
"""
|
"""
|
||||||
folder = Paths.get_xsm_thumb_path()
|
folder = Paths.get_xsm_thumb_path()
|
||||||
return send_file_or_fallback(folder, path.imgpath)
|
return send_file_or_fallback(folder, path.imgpath, pathhash=query.pathhash)
|
||||||
|
|
||||||
|
|
||||||
@api.get("/thumbnail/small/<imgpath>")
|
@api.get("/thumbnail/small/<imgpath>")
|
||||||
def send_sm_thumbnail(path: ImagePath):
|
def send_sm_thumbnail(path: ImagePath, query: ImageQuery):
|
||||||
"""
|
"""
|
||||||
Get small thumbnail (96px)
|
Get small thumbnail (96px)
|
||||||
"""
|
"""
|
||||||
folder = Paths.get_sm_thumb_path()
|
folder = Paths.get_sm_thumb_path()
|
||||||
return send_file_or_fallback(folder, path.imgpath)
|
return send_file_or_fallback(folder, path.imgpath, pathhash=query.pathhash)
|
||||||
|
|
||||||
|
|
||||||
@api.get("/thumbnail/medium/<imgpath>")
|
@api.get("/thumbnail/medium/<imgpath>")
|
||||||
def send_md_thumbnail(path: ImagePath):
|
def send_md_thumbnail(path: ImagePath, query: ImageQuery):
|
||||||
"""
|
"""
|
||||||
Get medium thumbnail (256px)
|
Get medium thumbnail (256px)
|
||||||
"""
|
"""
|
||||||
folder = Paths.get_md_thumb_path()
|
folder = Paths.get_md_thumb_path()
|
||||||
return send_file_or_fallback(folder, path.imgpath)
|
return send_file_or_fallback(folder, path.imgpath, pathhash=query.pathhash)
|
||||||
|
|
||||||
|
|
||||||
# ARTISTS
|
# ARTISTS
|
||||||
@@ -141,6 +239,7 @@ def send_playlist_image(path: PlaylistImagePath):
|
|||||||
folder = Paths.get_playlist_img_path()
|
folder = Paths.get_playlist_img_path()
|
||||||
return send_file_or_fallback(folder, path.imgpath, "playlist.svg")
|
return send_file_or_fallback(folder, path.imgpath, "playlist.svg")
|
||||||
|
|
||||||
|
|
||||||
# MIXES
|
# MIXES
|
||||||
@api.get("/mix/medium/<imgpath>")
|
@api.get("/mix/medium/<imgpath>")
|
||||||
def send_md_mix_image(path: ImagePath):
|
def send_md_mix_image(path: ImagePath):
|
||||||
|
|||||||
@@ -94,13 +94,19 @@ def log_track(body: LogTrackBody):
|
|||||||
if artist:
|
if artist:
|
||||||
artist.increment_playcount(duration, timestamp)
|
artist.increment_playcount(duration, timestamp)
|
||||||
|
|
||||||
track = TrackStore.trackhashmap.get(body.trackhash)
|
trackentry.increment_playcount(duration, timestamp)
|
||||||
if track:
|
track = trackentry.tracks[0]
|
||||||
track.increment_playcount(duration, timestamp)
|
|
||||||
|
|
||||||
lastfm = LastFmPlugin()
|
lastfm = LastFmPlugin()
|
||||||
|
|
||||||
if lastfm.enabled:
|
print(track.duration / 2, 240, body.duration, "\n")
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastfm.enabled
|
||||||
|
and track.duration > 30
|
||||||
|
and body.duration >= min(track.duration / 2, 240)
|
||||||
|
# SEE: https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble
|
||||||
|
):
|
||||||
lastfm.scrobble(trackentry.tracks[0], timestamp)
|
lastfm.scrobble(trackentry.tracks[0], timestamp)
|
||||||
|
|
||||||
return {"msg": "recorded"}, 201
|
return {"msg": "recorded"}, 201
|
||||||
@@ -350,7 +356,11 @@ def get_stats():
|
|||||||
if len(tracks) > 0
|
if len(tracks) > 0
|
||||||
else "—"
|
else "—"
|
||||||
),
|
),
|
||||||
tracks[0].image if len(tracks) > 0 else None,
|
(
|
||||||
|
tracks[0].image + "?pathhash=" + tracks[0].pathhash
|
||||||
|
if len(tracks) > 0
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
fav_count = FavoritesTable.count_favs_in_period(start_time, end_time)
|
fav_count = FavoritesTable.count_favs_in_period(start_time, end_time)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class UserConfig:
|
|||||||
"Crosby, Stills, Nash & Young",
|
"Crosby, Stills, Nash & Young",
|
||||||
"Smith & Thell",
|
"Smith & Thell",
|
||||||
"Peter, Paul & Mary",
|
"Peter, Paul & Mary",
|
||||||
|
"Simon & Garfunkel",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
genreSeparators: set[str] = field(default_factory=lambda: {"/", ";", "&"})
|
genreSeparators: set[str] = field(default_factory=lambda: {"/", ";", "&"})
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ def create_albums(_trackhashes: list[str] = []) -> list[tuple[Album, set[str]]]:
|
|||||||
"playduration": track.playduration,
|
"playduration": track.playduration,
|
||||||
"title": track.album,
|
"title": track.album,
|
||||||
"tracks": {track.trackhash},
|
"tracks": {track.trackhash},
|
||||||
|
"pathhash": track.pathhash,
|
||||||
"extra": {},
|
"extra": {},
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class Album:
|
|||||||
playcount: int
|
playcount: int
|
||||||
playduration: int
|
playduration: int
|
||||||
extra: dict
|
extra: dict
|
||||||
|
pathhash: str = ""
|
||||||
|
|
||||||
id: int = -1
|
id: int = -1
|
||||||
type: str = "album"
|
type: str = "album"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class Track:
|
|||||||
image: str = ""
|
image: str = ""
|
||||||
explicit: bool = False
|
explicit: bool = False
|
||||||
fav_userids: list[int] = field(default_factory=list)
|
fav_userids: list[int] = field(default_factory=list)
|
||||||
|
pathhash: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_favorite(self):
|
def is_favorite(self):
|
||||||
@@ -81,6 +82,7 @@ class Track:
|
|||||||
self.weakhash = create_hash(self.title, self.artists)
|
self.weakhash = create_hash(self.title, self.artists)
|
||||||
explicit_tag = self.extra.get("explicit", ["0"])
|
explicit_tag = self.extra.get("explicit", ["0"])
|
||||||
self.explicit = int(explicit_tag[0]) == 1
|
self.explicit = int(explicit_tag[0]) == 1
|
||||||
|
self.pathhash = create_hash(self.folder)
|
||||||
|
|
||||||
self.image = self.albumhash + ".webp"
|
self.image = self.albumhash + ".webp"
|
||||||
self.extra = {
|
self.extra = {
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ class Paths:
|
|||||||
def get_sm_mixes_img_path(cls):
|
def get_sm_mixes_img_path(cls):
|
||||||
return join(cls.get_mixes_img_path(), "small")
|
return join(cls.get_mixes_img_path(), "small")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_image_cache_path(cls):
|
||||||
|
return join(cls.get_img_path(), "cache")
|
||||||
|
|
||||||
|
|
||||||
# defaults
|
# defaults
|
||||||
class Defaults:
|
class Defaults:
|
||||||
|
|||||||
@@ -38,20 +38,8 @@ class AlbumMapEntry:
|
|||||||
|
|
||||||
|
|
||||||
class AlbumStore:
|
class AlbumStore:
|
||||||
# albums: list[Album] = CustomList()
|
|
||||||
albummap: dict[str, AlbumMapEntry] = {}
|
albummap: dict[str, AlbumMapEntry] = {}
|
||||||
|
|
||||||
# @staticmethod
|
|
||||||
# def create_album(track: Track):
|
|
||||||
# """
|
|
||||||
# Creates album object from a track
|
|
||||||
# """
|
|
||||||
# return Album(
|
|
||||||
# albumhash=track.albumhash,
|
|
||||||
# albumartists=track.albumartists, # type: ignore
|
|
||||||
# title=track.og_album,
|
|
||||||
# )
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_albums(cls, instance_key: str):
|
def load_albums(cls, instance_key: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
+3
-4
@@ -233,7 +233,7 @@ def get_track_group_stats(tracks: list[Track], is_album: bool = False):
|
|||||||
"toptrack",
|
"toptrack",
|
||||||
f"top track ({seconds_to_time_string(top_track.playduration)} listened)",
|
f"top track ({seconds_to_time_string(top_track.playduration)} listened)",
|
||||||
f"{top_track.title}",
|
f"{top_track.title}",
|
||||||
top_track.image,
|
top_track.image + "?pathhash=" + top_track.pathhash if top_track else None,
|
||||||
)
|
)
|
||||||
if top_track
|
if top_track
|
||||||
else StatItem(
|
else StatItem(
|
||||||
@@ -251,7 +251,7 @@ def get_track_group_stats(tracks: list[Track], is_album: bool = False):
|
|||||||
"playcount": 0,
|
"playcount": 0,
|
||||||
"playduration": 0,
|
"playduration": 0,
|
||||||
"title": track.album,
|
"title": track.album,
|
||||||
"image": track.image,
|
"image": track.image + "?pathhash=" + track.pathhash if track.image else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
albums_map[track.albumhash]["playcount"] += 1
|
albums_map[track.albumhash]["playcount"] += 1
|
||||||
@@ -268,8 +268,7 @@ def get_track_group_stats(tracks: list[Track], is_album: bool = False):
|
|||||||
"topalbum",
|
"topalbum",
|
||||||
f"top album ({seconds_to_time_string(top_album['playduration'])} listened)",
|
f"top album ({seconds_to_time_string(top_album['playduration'])} listened)",
|
||||||
f"{top_album['title']}",
|
f"{top_album['title']}",
|
||||||
top_album["image"],
|
top_album["image"])
|
||||||
)
|
|
||||||
if top_album
|
if top_album
|
||||||
else StatItem(
|
else StatItem(
|
||||||
"topalbum",
|
"topalbum",
|
||||||
|
|||||||
Reference in New Issue
Block a user