Compare commits
2 commits
199a44a8a7
...
8dc24a7763
Author | SHA1 | Date | |
---|---|---|---|
8dc24a7763 | |||
368d05349a |
8 changed files with 102 additions and 11 deletions
|
@ -24,6 +24,9 @@
|
||||||
.s-navigation--item.is-loading svg, .s-tag.is-loading svg, .s-sidebarwidget.is-loading svg {
|
.s-navigation--item.is-loading svg, .s-tag.is-loading svg, .s-sidebarwidget.is-loading svg {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
.s-btn.is-loading:not(.s-btn__icon) svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.s-btn__icon.is-loading {
|
.s-btn__icon.is-loading {
|
||||||
--_li-offset: 0.7em;
|
--_li-offset: 0.7em;
|
||||||
--_il-size: 1.5em;
|
--_il-size: 1.5em;
|
||||||
|
|
38
pug/home.pug
38
pug/home.pug
|
@ -42,26 +42,52 @@ html
|
||||||
p.fs-fine (you #[em should] download every mp3, though, because the Bandcamp TOS says they can take away your online access at any time)
|
p.fs-fine (you #[em should] download every mp3, though, because the Bandcamp TOS says they can take away your online access at any time)
|
||||||
|
|
||||||
h2 Why?
|
h2 Why?
|
||||||
p I mainly wanted to arrange my collection as labels instead of album covers. I've bought music from a variety of artists, but it's hard to find all of them amidst the enormous backlogs from Singto Conley and Lost Frog Productions. Giving each label the same amount of space makes my collection more fun to browse because I'm not scrolling past the same things over and over again.
|
p
|
||||||
p It's also faster to browse through because there's no "load more", everything shows up instantly.
|
| I mainly wanted to arrange my collection as labels instead of album covers. I've bought music from a variety of artists, but it's hard to find all of them amidst the enormous backlogs from
|
||||||
|
|
|
||||||
|
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?filter=Singto+Conley&filter_field=band_name&arrange=album&shape=grid")
|
||||||
|
span.s-tag--sponsor!= icons.get("people-tag", 18)
|
||||||
|
| Singto Conley
|
||||||
|
|
|
||||||
|
| and
|
||||||
|
|
|
||||||
|
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?filter=lostfrog&filter_field=band_url&arrange=album&shape=grid")
|
||||||
|
span.s-tag--sponsor!= icons.get("flower", 18)
|
||||||
|
| Lost Frog Productions
|
||||||
|
| . Giving each label the same amount of space makes my collection more fun to browse because I'm not scrolling past the same things over and over again.
|
||||||
p I also wanted as many different facets as possible for browsing my collection: albums, artists, labels, and tags. I want to be able to jump from one place to another. What else has this artist produced? Who has produced music in these tags? What else have I written reviews for?
|
p I also wanted as many different facets as possible for browsing my collection: albums, artists, labels, and tags. I want to be able to jump from one place to another. What else has this artist produced? Who has produced music in these tags? What else have I written reviews for?
|
||||||
|
p Skimming your collection is easy because there's no "load more", everything shows up instantly. The inline music player sticks around while you keep exploring.
|
||||||
|
|
||||||
|
h2 Tips and tricks
|
||||||
p
|
p
|
||||||
| All the little tags under things, like
|
| All the little tags under things, like
|
||||||
|
|
|
|
||||||
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?account=cloudrac3r&arrange=album&shape=grid&filter_field=band_url&filter=overheaven")
|
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?arrange=album&shape=grid&filter_field=band_url&filter=overheaven")
|
||||||
span.s-tag--sponsor!= icons.get("album", 18)
|
span.s-tag--sponsor!= icons.get("album", 18)
|
||||||
| 39
|
| 39
|
||||||
|
|
|
|
||||||
| the number of albums, or
|
| the number of albums, or
|
||||||
|
|
|
|
||||||
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?account=cloudrac3r&arrange=artist&shape=grid&filter_field=band_url&filter=lostfrog")
|
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?arrange=artist&shape=grid&filter_field=band_url&filter=lostfrog")
|
||||||
span.s-tag--sponsor!= icons.get("people-tag", 18)
|
span.s-tag--sponsor!= icons.get("people-tag", 18)
|
||||||
| 92
|
| 92
|
||||||
|
|
|
|
||||||
| the artists in a label, are clickable. If you click them it applies a filter and switches tabs to show you who they are.
|
| the artists in a label, are clickable. If you click them it applies a filter and switches tabs to show you who they are.
|
||||||
p While having a filter active, you can click the tabs at the top to use the same filter in a different facet. For example, you can go to the #[a(href="/cloudrac3r/?arrange=tag&shape=grid") tag cloud], search "breakcore", click it to #[a(href="/cloudrac3r/?filter=breakcore&filter_field=tag&filter_fuzzy=true&arrange=album&shape=grid") see all the breakcore albums] - there's a lot, right? Yes, but it's not because I'm enthusiastic about breakcore. Switch to #[a(href="/cloudrac3r/?filter=breakcore&filter_field=tag&arrange=label&shape=grid") the labels view]. Most of the breakcore is just from the same label, so there's not actually much diversity there.
|
p
|
||||||
|
| While having a filter active, you can click the tabs at the top to use the same filter in a different facet. For example, you can go to the
|
||||||
|
|
|
||||||
|
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?arrange=tag&shape=grid")
|
||||||
|
span.s-tag--sponsor!= icons.get("label", 18)
|
||||||
|
| tag cloud
|
||||||
|
| , search "breakcore", click it to see all the
|
||||||
|
|
|
||||||
|
a.s-tag.s-tag__sm.bg-black-200(href="/cloudrac3r/?filter=breakcore&filter_field=tag&filter_fuzzy=true&arrange=album&shape=grid")
|
||||||
|
span.s-tag--sponsor!= icons.get("album", 18)
|
||||||
|
| breakcore
|
||||||
|
|
|
||||||
|
| albums - there's a lot, right? Yes, but it's not because I'm enthusiastic about breakcore. From that page, switch to the labels tab. Turns out most of it's just from the same label, so there's not much diversity there.
|
||||||
p The tag cloud view takes this diversity into account automatically. Everything is grouped by label before being counted, to ensure that a large backlog from a single label does not take up an unworthy amount of "space" and crowd out the other singles that you're quite fond of. It's the same with the statistics heat bar. If you have #[a(href="http://localhost:2239/cloudrac3r/?arrange=album&shape=grid&filter_field=band_url&filter=louiezong") all of Louie Zong's albums], they'll only count for one red hot entry rather than over 150 entries.
|
p The tag cloud view takes this diversity into account automatically. Everything is grouped by label before being counted, to ensure that a large backlog from a single label does not take up an unworthy amount of "space" and crowd out the other singles that you're quite fond of. It's the same with the statistics heat bar. If you have #[a(href="http://localhost:2239/cloudrac3r/?arrange=album&shape=grid&filter_field=band_url&filter=louiezong") all of Louie Zong's albums], they'll only count for one red hot entry rather than over 150 entries.
|
||||||
p Oh, and there's an inline music player which sticks around while you keep exploring.
|
p Don't take the statistics too seriously! Some are a bit silly, like lonely tags, which are tags that only exist on a single item in your collection. Some are best guesses, like monetary value, which doesn't know if you chose to pay more than the minimum price or paid less via a discography discount or album code. Some are just for fun, like diversity, which shows whether you prefer popular or unpopular labels. Each label is one slot in the bar and it gets a hotter colour if it's more popular. Popularity is measured by the average number of other people who bought the same items as you.
|
||||||
|
|
||||||
footer.mt32.d-flex.fw-wrap.gx8.ai-center
|
footer.mt32.d-flex.fw-wrap.gx8.ai-center
|
||||||
a(href="https://cadence.moe") Created by Cadence
|
a(href="https://cadence.moe") Created by Cadence
|
||||||
|
|
|
@ -63,3 +63,19 @@
|
||||||
.s-progress--bar.bg-yellow-400(title=`${count.taste[1]} labels with 20-199 fans`)= count.taste[1]
|
.s-progress--bar.bg-yellow-400(title=`${count.taste[1]} labels with 20-199 fans`)= count.taste[1]
|
||||||
.s-progress--bar.bg-orange-400(title=`${count.taste[2]} labels with 200-1999 fans`)= count.taste[2]
|
.s-progress--bar.bg-orange-400(title=`${count.taste[2]} labels with 200-1999 fans`)= count.taste[2]
|
||||||
.s-progress--bar.bg-red-400(title=`${count.taste[3]} labels with >2000 fans`)= count.taste[3]
|
.s-progress--bar.bg-red-400(title=`${count.taste[3]} labels with >2000 fans`)= count.taste[3]
|
||||||
|
tr.s-sidebarwidget--item
|
||||||
|
th currency
|
||||||
|
td.w100
|
||||||
|
- var totalLabels = count.currencies.reduce((a, c) => a + c[1], 0)
|
||||||
|
- var palette = ["red", "orange", "green", "blue", "purple", "bronze"]
|
||||||
|
- var i = 0
|
||||||
|
.s-progress.d-grid.g2.h4.mtn6(style=`grid-template-columns: ${count.currencies.map(t => t[1] + "fr").join(" ")}`).bg-white.fc-black-400.fs-fine.lh-xxl
|
||||||
|
each c in count.currencies
|
||||||
|
.s-progress--bar.wmn-initial(title=`bought from ${c[1]} labels with ${c[0]}` class=`fc-${palette[i]}-400 bg-${palette[i++]}-400`)
|
||||||
|
- if (i >= palette.length) i = 0
|
||||||
|
if c[1] / totalLabels >= 0.08
|
||||||
|
= c[0]
|
||||||
|
else if c[1] / totalLabels >= 0.06
|
||||||
|
= c[0].slice(0, 2)
|
||||||
|
else
|
||||||
|
= c[0][0]
|
||||||
|
|
|
@ -45,6 +45,15 @@ html
|
||||||
.ml6 Switch account
|
.ml6 Switch account
|
||||||
li.s-menu--divider(role="separator")
|
li.s-menu--divider(role="separator")
|
||||||
li.s-menu--title Settings
|
li.s-menu--title Settings
|
||||||
|
if query && query.show
|
||||||
|
li(role="menuitem")
|
||||||
|
.s-block-link.d-flex.ai-center.py2
|
||||||
|
span.fl-grow1.d-flex.ai-center
|
||||||
|
!= icons.get("eye")
|
||||||
|
.pl6 Showing all items
|
||||||
|
a.s-btn.s-btn__filled.s-btn__sm.d-flex.ai-center(href=and({show: null}))
|
||||||
|
!= icons.get("undo", 16)
|
||||||
|
span.pl6 Undo
|
||||||
li(role="menuitem")
|
li(role="menuitem")
|
||||||
if arrange === "tag"
|
if arrange === "tag"
|
||||||
form.s-block-link.d-flex.ai-center(hx-post="/api/settings/location-tags" hx-trigger="change" hx-indicator="#location-tags-loading" hx-select="#tag-page" hx-target="#tag-page")
|
form.s-block-link.d-flex.ai-center(hx-post="/api/settings/location-tags" hx-trigger="change" hx-indicator="#location-tags-loading" hx-select="#tag-page" hx-target="#tag-page")
|
||||||
|
@ -144,6 +153,9 @@ html
|
||||||
include collection-stats.pug
|
include collection-stats.pug
|
||||||
|
|
||||||
main.fl-grow1
|
main.fl-grow1
|
||||||
|
if items && items.length >= 1000
|
||||||
|
//- a great deal of performance is spent on htmx's bfcache emulation when navigating away from large pages
|
||||||
|
div(hx-history="false")
|
||||||
block view
|
block view
|
||||||
|
|
||||||
#player-container.ps-fixed.r16.ws340.z-modal(class="md:t64 md:l16 md:r16 md:b16" hx-preserve="true")
|
#player-container.ps-fixed.r16.ws340.z-modal(class="md:t64 md:l16 md:r16 md:b16" hx-preserve="true")
|
||||||
|
|
17
pug/too-many-items.pug
Normal file
17
pug/too-many-items.pug
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
extends includes/layout.pug
|
||||||
|
|
||||||
|
block view
|
||||||
|
.mx-auto.w100.wmx11.fs-body1.d-flex.jc-center#content
|
||||||
|
.s-notice.s-notice__warning
|
||||||
|
.d-flex.gx16.ai-center
|
||||||
|
!= icons.get("warning-triangle")
|
||||||
|
div This page has #{itemCount} items, which might slow down your browser.
|
||||||
|
.d-flex.gx8.mt16
|
||||||
|
button.s-btn.s-btn__filled(hx-get=and({}) hx-headers='{"BCEX-Show": "true"}') Show them anyway
|
||||||
|
a.s-btn.s-btn__outlined(href=and({show: true})) Always show
|
||||||
|
p.mt16 Other things to try:
|
||||||
|
ul.mb4
|
||||||
|
li Search for specific items
|
||||||
|
if arrange !== "label"
|
||||||
|
li View items grouped together, like the #[a(href=and({arrange: "label"})) labels tab]
|
||||||
|
li Use #[a(href=and({shape: "list"}) hx-headers='{"BCEX-Show": "true"}') list view], which may be faster
|
|
@ -1,6 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const {defineEventHandler, getQuery, getValidatedQuery, sendRedirect, createError, getValidatedRouterParams, getCookie} = require("h3")
|
const {defineEventHandler, getQuery, getValidatedQuery, sendRedirect, createError, getValidatedRouterParams, getCookie, getRequestHeader} = require("h3")
|
||||||
const {router, db, sync} = require("../passthrough")
|
const {router, db, sync} = require("../passthrough")
|
||||||
|
|
||||||
/** @type {import("../pug-sync")} */
|
/** @type {import("../pug-sync")} */
|
||||||
|
@ -73,7 +73,7 @@ router.get("/:account/", defineEventHandler({
|
||||||
handler: async event => {
|
handler: async event => {
|
||||||
const {account} = await getValidatedRouterParams(event, schema.schema.account.parse)
|
const {account} = await getValidatedRouterParams(event, schema.schema.account.parse)
|
||||||
try {
|
try {
|
||||||
var {arrange, shape, filter, filter_field, filter_fuzzy} = await getValidatedQuery(event, schema.schema.appQuery.parse)
|
var {arrange, shape, filter, filter_field, filter_fuzzy, show} = await getValidatedQuery(event, schema.schema.appQuery.parse)
|
||||||
if (filter_field === "why" && arrange !== "album") throw new Error("filter not compatible with arrangement")
|
if (filter_field === "why" && arrange !== "album") throw new Error("filter not compatible with arrangement")
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return sendRedirect(event, "?arrange=album&shape=grid", 302)
|
return sendRedirect(event, "?arrange=album&shape=grid", 302)
|
||||||
|
@ -125,6 +125,12 @@ router.get("/:account/", defineEventHandler({
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show ||= getRequestHeader(event, "BCEX-Show")
|
||||||
|
const itemWarningLimit = arrange === "track" ? 5000 : 1000
|
||||||
|
if (items.length >= itemWarningLimit && !show) {
|
||||||
|
return pugSync.render(event, "too-many-items.pug", {itemCount: items.length, account, query})
|
||||||
|
}
|
||||||
|
|
||||||
const locals = {
|
const locals = {
|
||||||
items,
|
items,
|
||||||
account,
|
account,
|
||||||
|
|
|
@ -13,12 +13,21 @@ const currencyExchange = new Map([
|
||||||
["AUD", 0.63],
|
["AUD", 0.63],
|
||||||
["BRL", 0.17],
|
["BRL", 0.17],
|
||||||
["CAD", 0.7],
|
["CAD", 0.7],
|
||||||
|
["CZK", 0.045],
|
||||||
["CHF", 1.13],
|
["CHF", 1.13],
|
||||||
|
["DKK", 0.15],
|
||||||
["EUR", 1.08],
|
["EUR", 1.08],
|
||||||
["GBP", 1.3],
|
["GBP", 1.3],
|
||||||
|
["HKD", 0.13],
|
||||||
|
["HUF", 0.0028],
|
||||||
|
["ILS", 0.27],
|
||||||
|
["MXN", 0.05],
|
||||||
["JPY", 0.0067],
|
["JPY", 0.0067],
|
||||||
["NOK", 0.1],
|
["NOK", 0.1],
|
||||||
["NZD", 0.57],
|
["NZD", 0.57],
|
||||||
|
["PLN", 0.27],
|
||||||
|
["SEK", 0.1],
|
||||||
|
["SGD", 0.76],
|
||||||
["USD", 1],
|
["USD", 1],
|
||||||
])
|
])
|
||||||
const currencies = [...currencyExchange.keys()]
|
const currencies = [...currencyExchange.keys()]
|
||||||
|
@ -32,7 +41,7 @@ pugSync.beforeInclude("includes/layout.pug", async (from, event, locals) => {
|
||||||
pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {account, currency}) => {
|
pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {account, currency}) => {
|
||||||
let displayCurrency = currency || getCookie(event, "bcex-currency") || ""
|
let displayCurrency = currency || getCookie(event, "bcex-currency") || ""
|
||||||
if (!currencyExchange.has(displayCurrency)) displayCurrency = "NZD"
|
if (!currencyExchange.has(displayCurrency)) displayCurrency = "NZD"
|
||||||
const currencyRoundTo = displayCurrency === "JPY" ? 1000 : 10
|
const currencyRoundTo = (currencyExchange.get(displayCurrency) || 1) < 0.01 ? 1000 : 10
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count: {
|
count: {
|
||||||
|
@ -51,7 +60,8 @@ pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {acco
|
||||||
return (currencyExchange.get(c.currency) || 0.6) * c.price / (currencyExchange.get(displayCurrency) || 1) / currencyRoundTo
|
return (currencyExchange.get(c.currency) || 0.6) * c.price / (currencyExchange.get(displayCurrency) || 1) / currencyRoundTo
|
||||||
}).reduce((a, c) => a + c, 0)) * currencyRoundTo,
|
}).reduce((a, c) => a + c, 0)) * currencyRoundTo,
|
||||||
displayCurrency,
|
displayCurrency,
|
||||||
taste: db.prepare("with popularity (a) as (select avg(also_collected_count) from item WHERE account = ? group by band_url) select sum(iif(a >= 0 and a < 20, 1, 0)) as cold, sum(iif(a >= 20 and a < 200, 1, 0)) as warm, sum(iif(a >= 200 and a < 2000, 1, 0)) as hot, sum(iif(a >= 2000, 1, 0)) as supernova from popularity").raw().get(account)
|
taste: db.prepare("with popularity (a) as (select avg(also_collected_count) from item WHERE account = ? group by band_url) select sum(iif(a >= 0 and a < 20, 1, 0)) as cold, sum(iif(a >= 20 and a < 200, 1, 0)) as warm, sum(iif(a >= 200 and a < 2000, 1, 0)) as hot, sum(iif(a >= 2000, 1, 0)) as supernova from popularity").raw().get(account),
|
||||||
|
currencies: db.prepare("select currency, count(*) as count from (select currency, band_url from item where account = ? group by band_url) group by currency order by count desc").raw().all(account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,7 +10,8 @@ const schema = {
|
||||||
shape: z.enum(["grid", "list"]),
|
shape: z.enum(["grid", "list"]),
|
||||||
filter_field: z.enum(["band_name", "band_url", "item_title", "item_id", "tag", "why"]).optional(),
|
filter_field: z.enum(["band_name", "band_url", "item_title", "item_id", "tag", "why"]).optional(),
|
||||||
filter: z.string().regex(/^[^%]+$/).optional(),
|
filter: z.string().regex(/^[^%]+$/).optional(),
|
||||||
filter_fuzzy: z.enum(["true"]).optional()
|
filter_fuzzy: z.enum(["true"]).optional(),
|
||||||
|
show: z.string().optional()
|
||||||
}),
|
}),
|
||||||
account: z.object({
|
account: z.object({
|
||||||
account
|
account
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue