Compare commits

..

2 commits

Author SHA1 Message Date
70ce8ab72b Add page titles 2025-04-04 20:39:40 +13:00
6fb5a943db Fix track data import, clean up code 2025-04-04 20:39:04 +13:00
12 changed files with 76 additions and 37 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
scripts/*.har
scripts/collection_*
node_modules
.vscode

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,11 +1,14 @@
extends includes/layout.pug
block title
- title = `Albums | ${title}`
block view
.mx-auto.w100.wmx11.fs-body1#content
.d-grid.gx8.gy12.jc-center(style="grid-template-columns: repeat(auto-fit, 210px)")
.d-grid.gx8.gy12.jc-center.break-word(style="grid-template-columns: repeat(auto-fit, 210px)")
each item in items
div
a.album-grid-link(href=item.item_url)
a.album-grid-link(href=item.item_url target="_blank")
img(loading="lazy" src=item.item_art_url width=210 height=210)
p.fs-body3.mb8= item.item_title
.d-flex.fw-wrap.g4

View file

@ -1,5 +1,8 @@
extends includes/layout.pug
block title
- title = `Artists | ${title}`
block view
.mx-auto.w100.wmx9.fs-body1#content
.d-flex.fd-column.g4
@ -24,5 +27,5 @@ block view
span.s-tag--sponsor!= icons.get("flower", 16)
= label
each preview in item.previews
a.d-flex(href=preview.item_url)
a.d-flex(href=preview.item_url target="_blank")
img(loading="lazy" src=preview.item_art_url width=210 height=210 style="height: auto; width: auto; max-height: 70px")

View file

@ -8,10 +8,17 @@ mixin navi(key, value, icon, text)
doctype html
html
- let searchText = !filter_field ? null : filter_field === "item_id" ? items[0].item_title : filter
head
meta(charset="utf-8")
<meta name="viewport" content="width=device-width, initial-scale=1" />
title BC Explorer
- let title = "BC Explorer"
block title
if searchText
- title = `${searchText} | ${title}`
title#title= title
link(rel="icon" href="/favicon.png")
link(rel="stylesheet" type="text/css" href="/static/stacks.min.css")
script(src="/static/htmx.js")
script(src="/static/wordcloud.js")
@ -98,11 +105,7 @@ html
if filter && filter_field
.s-sidebarwidget.s-sidebarwidget__blue.d-flex.ai-center.gx16.jc-space-between.p8.pl16
!= icons.get("search")
.fl-grow1= `Searching for `
if filter_field === "item_id"
strong= items[0].item_title
else
strong= filter
.fl-grow1 Searching for #[strong= searchText]
a.s-btn.s-notice--btn(href=and({filter: null, filter_field: null, filter_fuzzy: null})) Clear
else
form.d-flex.ai-stretch.gx8.jc-space-between.baw0(hx-indicator="#search-submit")

View file

@ -1,4 +1,4 @@
if downloader.total > 0
if downloader.total > 0 || downloader.outcome
#tag-download
if !downloader.running && !downloader.outcome
form.s-notice.s-notice__info.d-flex.ai-center.p8.gx16.pl16(role="status" hx-swap="outerHTML" hx-target="#tag-download" hx-post="/api/tag-download")
@ -15,6 +15,7 @@ if downloader.total > 0
#tag-status-indicator
p.mt12 You can keep using BC Explorer while this continues in the background.
- let percentage = `${Math.round(downloader.processed/downloader.total*100)}%`
title#title(hx-swap-oob="true") #{percentage} | Tags | BC Explorer
.s-progress.mt16
.s-progress--bar(style=`width: ${percentage}`)
.d-flex.jc-space-between.fs-fine
@ -23,12 +24,14 @@ if downloader.total > 0
else if downloader.outcome === "Success"
.s-notice.s-notice__success.p8.gx16.pl16.d-flex.ai-center.wmn3
title#title(hx-swap-oob="true") * Tags downloaded! | BC Explorer
!= icons.get("cloud-check")
.fl-grow1 Tags downloaded.
- downloadManager.resolve(account)
- downloader.resolve()
a.s-btn.s-btn__outlined(href="") Refresh
else
.s-notice.s-notice__danger.p8.gx16.pl16.d-flex.ai-center.wmn3
title#title(hx-swap-oob="true") * Tag download failed! | BC Explorer
!= icons.get("cloud-xmark")
.fl-grow1= downloader.outcome

View file

@ -1,5 +1,8 @@
extends includes/layout.pug
block title
- title = `Labels | ${title}`
block view
.mx-auto.w100.wmx9.fs-body1#content
.d-flex.fd-column.g4
@ -24,5 +27,5 @@ block view
span.s-tag--sponsor!= icons.get("compact-disc", 16)
= item.total_duration
each preview in item.previews
a.d-flex(href=preview.item_url)
a.d-flex(href=preview.item_url target="_blank")
img(loading="lazy" src=preview.item_art_url width=210 height=210 style="height: auto; width: auto; max-height: 70px")

View file

@ -1,5 +1,8 @@
extends includes/layout.pug
block title
- title = `Tags | ${title}`
block view
script
| var items =

View file

@ -1,5 +1,8 @@
extends includes/layout.pug
block title
- title = `Tracks | ${title}`
block view
.mx-auto.w100.wmx11#content
.s-table-container
@ -18,7 +21,7 @@ block view
| ☆
else
= item.track_number || "-"
td: a(href=item.item_url)= item.title
td: a(href=item.item_url target="_blank")= item.title
td= item.artist
td= item.item_title
- let label = item.band_url.replace(/https?:\/\/(.*?)\.bandcamp\.com.*/, "$1")

View file

@ -16,7 +16,7 @@ but the idea is you can more easily search your whole collection and play it str
## import more reliable statistics
the default data is pretty close, but you can do this to get the exact data
by default, the data is mostly correct, but you can do this to get the exact data
1. log into bandcamp in your browser
2. in the top right, click the button to view your collection, should take you to a url like https://bandcamp.com/cloudrac3r

View file

@ -8,24 +8,33 @@ const {sync, db, from, router} = require("../passthrough")
const pugSync = sync.require("../pug-sync")
const insertTag = db.prepare("INSERT OR IGNORE INTO item_tag (account, item_id, tag) VALUES (?, ?, ?)")
const insertTrack = db.prepare("INSERT OR IGNORE INTO track (account, item_id, track_id, title, artist, track_number, duration) VALUES (?, ?, ?, ?, ?, ?, ?)")
const insertTrack = db.prepare("INSERT OR IGNORE INTO track (account, item_id, track_id, title, artist, track_number, duration) VALUES (@account, @item_id, @track_id, @title, @artist, @track_number, @duration)")
class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) {
constructor(account) {
super()
this.account = account
this.processed = 0
this.untaggedItems = db.prepare("SELECT account, item_id, item_title, item_url, band_name FROM item LEFT JOIN item_tag USING (account, item_id) WHERE account = ? AND tag IS NULL").all(account)
this.untaggedItems = []
this.total = this.untaggedItems.length
this.running = false
this.outcome = null
this.check()
}
check() {
if (this.running) return // don't reduce the total while items are being processed, this will break the progress bar
this.untaggedItems = db.prepare("SELECT account, item_id, item_title, item_url, band_name FROM item LEFT JOIN item_tag USING (account, item_id) WHERE account = ? AND tag IS NULL").all(this.account)
this.total = this.untaggedItems.length
}
async _start() {
if (this.running) return
this.running = true
this.outcome = null
this.processed = 0
try {
for (const {account, item_id, item_title, item_url, band_name} of this.untaggedItems) {
for (const {account, item_id, item_type, item_title, item_url, band_name} of this.untaggedItems) {
const res = await fetch(item_url)
// delete unreachable items, otherwise it will perpetually try to download tags for them
@ -47,21 +56,18 @@ class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) {
})()
// @ts-ignore
const tracks = [...doc.querySelectorAll(".track_row_view").cache]
const tracklist = JSON.parse(doc.querySelector("script[data-tralbum]").getAttribute("data-tralbum")).trackinfo
db.transaction(() => {
for (const track of tracks) {
const track_number = parseInt(track.querySelector(".track_number").textContent)
let title = track.querySelector(".track-title").textContent
let artist = band_name
const match = title.match(/^([^-]*) - (.*)$/)
if (match) {
artist = match[1]
title = match[2]
}
const duration = track.querySelector(".time").textContent.split(":").reverse().reduce((a, c, i) => 60**i * c + a, 0)
console.log(track_number, title, artist, duration)
if (!track_number || !title || !artist || !duration) continue
insertTrack.run(account, item_id, track_number, title, artist, track_number, duration)
for (const track of tracklist) {
insertTrack.run({
account,
item_id,
track_id: track.id,
title: track.title,
artist: track.artist || band_name,
track_number: track.track_num,
duration: track.duration
})
}
})()
@ -75,6 +81,10 @@ class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) {
this.running = false
}
}
resolve() {
this.outcome = null
}
}
const downloadManager = new class {
@ -83,14 +93,13 @@ const downloadManager = new class {
/** @param {string} account */
check(account) {
return this.inProgressTagDownloads.get(account) || (() => {
const downloader = this.inProgressTagDownloads.get(account) || (() => {
const downloader = new TagDownloader(account)
this.inProgressTagDownloads.set(account, downloader)
setTimeout(() => {
this.resolve(account)
})
return downloader
})()
downloader.check()
return downloader
}
/** @param {string} account */
@ -116,13 +125,13 @@ const schema = z.object({
router.get("/api/tag-download", defineEventHandler(async event => {
const {account} = await getValidatedQuery(event, schema.parse)
const downloader = downloadManager.check(account)
return pugSync.render(event, "includes/tag-status.pug", {downloadManager, downloader, account})
return pugSync.render(event, "includes/tag-status.pug", {downloader, account})
}))
router.post("/api/tag-download", defineEventHandler(async event => {
const {account} = await readValidatedBody(event, schema.parse)
const downloader = downloadManager.start(account)
return pugSync.render(event, "includes/tag-status.pug", {downloadManager, downloader, account})
return pugSync.render(event, "includes/tag-status.pug", {downloader, account})
}))
module.exports.downloadManager = downloadManager

View file

@ -66,5 +66,13 @@ router.get("/static/wordcloud.js", defineEventHandler({
}
}))
router.get("/favicon.png", defineEventHandler({
handler: async event => {
handleCacheHeaders(event, {maxAge: 86400})
defaultContentType(event, "text/javascript")
return fs.promises.readFile("public/favicon.png")
}
}))
createServer(toNodeListener(app)).listen(2239)
console.log("running on http://localhost:2239")