Compare commits
2 commits
cf6310b89a
...
70ce8ab72b
Author | SHA1 | Date | |
---|---|---|---|
70ce8ab72b | |||
6fb5a943db |
12 changed files with 76 additions and 37 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@
|
|||
scripts/*.har
|
||||
scripts/collection_*
|
||||
node_modules
|
||||
.vscode
|
||||
|
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
extends includes/layout.pug
|
||||
|
||||
block title
|
||||
- title = `Tags | ${title}`
|
||||
|
||||
block view
|
||||
script
|
||||
| var items =
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
8
start.js
8
start.js
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue