Pre-release version v0.0.13 (#52)

* feat: search/merge items by id

* feat: update track duration using musicbrainz

* chore: changelog

* fix: make username updates case insensitive

* feat: add minutes listened to ui and fix image drop

* chore: changelog

* fix: embed db migrations (#37)

* feat: Add support for ARM in publish workflow (#51)

* chore: changelog

* docs: search by id and custom theme support

---------

Co-authored-by: potatoattack <lvl70nub@gmail.com>
Co-authored-by: Benjamin Jonard <benjaminjonard@users.noreply.github.com>
This commit is contained in:
Gabe Farrell 2025-07-26 15:57:46 -04:00 committed by GitHub
parent 5537b6fb89
commit 5419178012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1252 additions and 100 deletions

View file

@ -288,7 +288,7 @@ type Stats = {
track_count: number
album_count: number
artist_count: number
hours_listened: number
minutes_listened: number
}
type SearchResponse = {
albums: Album[]

View file

@ -26,7 +26,7 @@ export default function AllTimeStats() {
<div>
<h2>All Time Stats</h2>
<div>
<span className={numberClasses}>{data.hours_listened}</span> Hours Listened
<span className={numberClasses} title={data.minutes_listened + " minutes"}>{Math.floor(data.minutes_listened / 60)}</span> Hours Listened
</div>
<div>
<span className={numberClasses}>{data.listen_count}</span> Plays

View file

@ -3,11 +3,10 @@ import { useEffect } from 'react';
interface Props {
itemType: string,
id: number,
onComplete: Function
}
export default function ImageDropHandler({ itemType, id, onComplete }: Props) {
export default function ImageDropHandler({ itemType, onComplete }: Props) {
useEffect(() => {
const handleDragOver = (e: DragEvent) => {
console.log('dragover!!')
@ -25,7 +24,11 @@ export default function ImageDropHandler({ itemType, id, onComplete }: Props) {
const formData = new FormData();
formData.append('image', imageFile);
formData.append(itemType.toLowerCase()+'_id', String(id))
const pathname = window.location.pathname;
const segments = pathname.split('/');
const filteredSegments = segments.filter(segment => segment !== '');
const lastSegment = filteredSegments[filteredSegments.length - 1];
formData.append(itemType.toLowerCase()+'_id', lastSegment)
replaceImage(formData).then((r) => {
if (r.status >= 200 && r.status < 300) {
onComplete()

View file

@ -23,12 +23,12 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
}
}
if (data === undefined) {
if (!data) {
return <></>
}
return (
<div className="w-full">
{ data.artists.length > 0 &&
{ data.artists && data.artists.length > 0 &&
<>
<h3 className={hClasses}>Artists</h3>
<div className={classes}>
@ -52,7 +52,7 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
</div>
</>
}
{ data.albums.length > 0 &&
{ data.albums && data.albums.length > 0 &&
<>
<h3 className={hClasses}>Albums</h3>
<div className={classes}>
@ -77,7 +77,7 @@ export default function SearchResults({ data, onSelect, selectorMode }: Props) {
</div>
</>
}
{ data.tracks.length > 0 &&
{ data.tracks && data.tracks.length > 0 &&
<>
<h3 className={hClasses}>Tracks</h3>
<div className={classes}>

View file

@ -43,7 +43,7 @@ export default function Album() {
}}
subContent={<div className="flex flex-col gap-2 items-start">
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
{<p>{timeListenedString(album.time_listened)}</p>}
{<p title={Math.floor(album.time_listened / 60) + " minutes"}>{timeListenedString(album.time_listened)}</p>}
</div>}
>
<div className="mt-10">

View file

@ -49,7 +49,7 @@ export default function Artist() {
}}
subContent={<div className="flex flex-col gap-2 items-start">
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
{<p>{timeListenedString(artist.time_listened)}</p>}
{<p title={Math.floor(artist.time_listened / 60) + " minutes"}>{timeListenedString(artist.time_listened)}</p>}
</div>}
>
<div className="mt-10">

View file

@ -61,7 +61,7 @@ export default function MediaLayout(props: Props) {
transition: '1000',
}}
>
<ImageDropHandler itemType={props.type.toLowerCase() === 'artist' ? 'artist' : 'album'} id={props.imgItemId} onComplete={replaceImageCallback} />
<ImageDropHandler itemType={props.type.toLowerCase() === 'artist' ? 'artist' : 'album'} onComplete={replaceImageCallback} />
<title>{title}</title>
<meta property="og:title" content={title} />
<meta

View file

@ -46,7 +46,7 @@ export default function Track() {
subContent={<div className="flex flex-col gap-2 items-start">
<Link to={`/album/${track.album_id}`}>appears on {album.title}</Link>
{track.listen_count && <p>{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}</p>}
{<p>{timeListenedString(track.time_listened)}</p>}
{<p title={Math.floor(track.time_listened / 60) + " minutes"}>{timeListenedString(track.time_listened)}</p>}
</div>}
>
<div className="mt-10">