Compare commits

..

No commits in common. "70ce8ab72b8cc96743f628b2ed31a846e6fe5b48" and "cf6310b89a10065a7a9e27d3389ba10bde2b7fa5" have entirely different histories.

12 changed files with 37 additions and 76 deletions

1
.gitignore vendored
View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,5 @@
extends includes/layout.pug extends includes/layout.pug
block title
- title = `Tracks | ${title}`
block view block view
.mx-auto.w100.wmx11#content .mx-auto.w100.wmx11#content
.s-table-container .s-table-container
@ -21,7 +18,7 @@ block view
| ☆ | ☆
else else
= item.track_number || "-" = item.track_number || "-"
td: a(href=item.item_url target="_blank")= item.title td: a(href=item.item_url)= item.title
td= item.artist td= item.artist
td= item.item_title td= item.item_title
- let label = item.band_url.replace(/https?:\/\/(.*?)\.bandcamp\.com.*/, "$1") - 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 ## import more reliable statistics
by default, the data is mostly correct, but you can do this to get the exact data the default data is pretty close, but you can do this to get the exact data
1. log into bandcamp in your browser 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 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,33 +8,24 @@ const {sync, db, from, router} = require("../passthrough")
const pugSync = sync.require("../pug-sync") const pugSync = sync.require("../pug-sync")
const insertTag = db.prepare("INSERT OR IGNORE INTO item_tag (account, item_id, tag) VALUES (?, ?, ?)") 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 (@account, @item_id, @track_id, @title, @artist, @track_number, @duration)") const insertTrack = db.prepare("INSERT OR IGNORE INTO track (account, item_id, track_id, title, artist, track_number, duration) VALUES (?, ?, ?, ?, ?, ?, ?)")
class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) { class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) {
constructor(account) { constructor(account) {
super() super()
this.account = account this.account = account
this.processed = 0 this.processed = 0
this.untaggedItems = [] 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.total = this.untaggedItems.length this.total = this.untaggedItems.length
this.running = false this.running = false
this.outcome = null 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() { async _start() {
if (this.running) return if (this.running) return
this.running = true this.running = true
this.outcome = null
this.processed = 0
try { try {
for (const {account, item_id, item_type, item_title, item_url, band_name} of this.untaggedItems) { for (const {account, item_id, item_title, item_url, band_name} of this.untaggedItems) {
const res = await fetch(item_url) const res = await fetch(item_url)
// delete unreachable items, otherwise it will perpetually try to download tags for them // delete unreachable items, otherwise it will perpetually try to download tags for them
@ -56,18 +47,21 @@ class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) {
})() })()
// @ts-ignore // @ts-ignore
const tracklist = JSON.parse(doc.querySelector("script[data-tralbum]").getAttribute("data-tralbum")).trackinfo const tracks = [...doc.querySelectorAll(".track_row_view").cache]
db.transaction(() => { db.transaction(() => {
for (const track of tracklist) { for (const track of tracks) {
insertTrack.run({ const track_number = parseInt(track.querySelector(".track_number").textContent)
account, let title = track.querySelector(".track-title").textContent
item_id, let artist = band_name
track_id: track.id, const match = title.match(/^([^-]*) - (.*)$/)
title: track.title, if (match) {
artist: track.artist || band_name, artist = match[1]
track_number: track.track_num, title = match[2]
duration: track.duration }
}) 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)
} }
})() })()
@ -81,10 +75,6 @@ class TagDownloader extends sync.reloadClassMethods(() => TagDownloader) {
this.running = false this.running = false
} }
} }
resolve() {
this.outcome = null
}
} }
const downloadManager = new class { const downloadManager = new class {
@ -93,13 +83,14 @@ const downloadManager = new class {
/** @param {string} account */ /** @param {string} account */
check(account) { check(account) {
const downloader = this.inProgressTagDownloads.get(account) || (() => { return this.inProgressTagDownloads.get(account) || (() => {
const downloader = new TagDownloader(account) const downloader = new TagDownloader(account)
this.inProgressTagDownloads.set(account, downloader) this.inProgressTagDownloads.set(account, downloader)
setTimeout(() => {
this.resolve(account)
})
return downloader return downloader
})() })()
downloader.check()
return downloader
} }
/** @param {string} account */ /** @param {string} account */
@ -125,13 +116,13 @@ const schema = z.object({
router.get("/api/tag-download", defineEventHandler(async event => { router.get("/api/tag-download", defineEventHandler(async event => {
const {account} = await getValidatedQuery(event, schema.parse) const {account} = await getValidatedQuery(event, schema.parse)
const downloader = downloadManager.check(account) const downloader = downloadManager.check(account)
return pugSync.render(event, "includes/tag-status.pug", {downloader, account}) return pugSync.render(event, "includes/tag-status.pug", {downloadManager, downloader, account})
})) }))
router.post("/api/tag-download", defineEventHandler(async event => { router.post("/api/tag-download", defineEventHandler(async event => {
const {account} = await readValidatedBody(event, schema.parse) const {account} = await readValidatedBody(event, schema.parse)
const downloader = downloadManager.start(account) const downloader = downloadManager.start(account)
return pugSync.render(event, "includes/tag-status.pug", {downloader, account}) return pugSync.render(event, "includes/tag-status.pug", {downloadManager, downloader, account})
})) }))
module.exports.downloadManager = downloadManager module.exports.downloadManager = downloadManager

View file

@ -66,13 +66,5 @@ 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) createServer(toNodeListener(app)).listen(2239)
console.log("running on http://localhost:2239") console.log("running on http://localhost:2239")