From 6fb5a943dbb66b00c7840346dacb7bc978ac6d6c Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 4 Apr 2025 20:39:04 +1300 Subject: [PATCH 1/2] Fix track data import, clean up code --- readme.md | 2 +- routes/load-tags.js | 55 ++++++++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/readme.md b/readme.md index f18bb3e..5545bf0 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/routes/load-tags.js b/routes/load-tags.js index a061158..24cb492 100644 --- a/routes/load-tags.js +++ b/routes/load-tags.js @@ -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 From 70ce8ab72b8cc96743f628b2ed31a846e6fe5b48 Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Fri, 4 Apr 2025 20:39:40 +1300 Subject: [PATCH 2/2] Add page titles --- .gitignore | 1 + public/favicon.png | Bin 0 -> 4310 bytes pug/album_grid.pug | 7 +++++-- pug/artist_grid.pug | 5 ++++- pug/includes/layout.pug | 15 +++++++++------ pug/includes/tag-status.pug | 7 +++++-- pug/label_grid.pug | 5 ++++- pug/tag_grid.pug | 3 +++ pug/track_list.pug | 5 ++++- start.js | 8 ++++++++ 10 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 public/favicon.png diff --git a/.gitignore b/.gitignore index 2e018b6..a4494b6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ scripts/*.har scripts/collection_* node_modules +.vscode diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..de4ca714f3c094d8fe0d2def01a55f53dd12fc69 GIT binary patch literal 4310 zcmV;{5Gn78P){ z85C$8J62tzYq~~SH3X@bDA>wWK*bCsqX@B5E=o`m`imK9|5RjA?i^k(&l4{qm0g}okAKCPOcS*Fmheqj>r1{jt9!5Bl$3@G>MJ5dodL$@6~-UIn{5cQ6=nfvJ1R)nt*UQ}!P;RB zjfH#encd>3o)C!&157E{HK?|^{wjs*QT_=S=Cr2nk*`7jY@)B#wj{3CJEpffttc3- zHh?d0<3t1FHWhM#6sPrsNR_~=f-I|;m$B1nF@bQU0emZpGcol6fvW*=T8{-3i}Ks5 z+{`s@n?$uBK-H6Kn5gYsv(DO`mi!Z-;p4~m!b_bAx0#9;N>hA>kg)JQn9Gn(45V6r+<{d3E5vyWQcu;m8u6%-DUCeIrnS6SNd07v2dHgDWD zbvIr7jg{?MYJikKZcZVt^{>G9Eo(H!2?Rc6TYH@n=5#w;;F+KZc#O^<9sImZG`F@p1jsz0+jH@mX04fXjoFlpuHVWuH# zV}l=2CWv^~eJ8JQL|+@~lL4j_>>A|tx4us7nePPW$BB2<#An|g+?PheHh?U~sBNx) z3AnN^0%C-Zz-8YI8~TbA6nMgIBWwdCr*C>vl{w*FAHy=P@inDA7VgDiO#)N%is!26 z%5Xc4VL2+I*H+$=_2*D84A}t3zu5TzNDQ^J7**iMwEHt^ z8hTkxPS*eS=Hy=80mdvVNmS*(i4A@UjR=8J2B`ZAnnCXlT1Ul5nH_*7KXs}6H<6^ z+Bnh@2c7cyntiq0Uig_ECTR3~{pZ%*lzpH_eZg8WMV=xy_+c?KZ3OT9Xd3^SF|pgh z2l&yrQRJMRYDf1&yngS~!A|P&4&ci#o-L|>wN%ys;G7YOESf%^D^CATug?Ft_As;m zXPYfr1ihkiUe813%l_ zcj?CHfnXE7-m+?MjfJXCMUaPqjop6M-5lU}(B~anRYf_OX(M>_f^^OsIiyeRR5Tsq z($!mO_S>@R`vis>7gglW*wXcQw|4+l?z2@*6f<#n0t=>&~4t2_Wx55 zksIh_fD?~^@p*;uP2#P>wMiK5BRL_C1yjc{*TM;2AR1fSxMcNKYMLF(A=!|eI5K6& zoJq%^(;eUl-fS6QgKy^qFA&dcKj7rx1B|F`uAd3K3!MybJooN^!0riNAS#-UvFy`> z5$i7ys?4IR0TgCCrYM5gJ;CdV-$W?r(+g$;zk=g)fKmA$jPYn&j|&H)ok&ga0M1Ta5$T8mKT(W8_wMX4qIK<;O(hX5_lG8%0Ai-;}aC*AaGEU;z?FU>t_yGPmon)B8c&CL} z!JU)GxZ;`Lfm?z;Jt1nQ8={PJT1a0R6(7f4lTUYA#!0v+=o1(*#v2G{IIW|vJTPsX zZ|t=!m7i^h>No?$=d_T%FmZSyb0@m1pxDxqh_3Vr8xxWZ&?KjY^o2(+ z81HC=uPf_Z8T9EMfg}SM>9mk=$x0j1>+M&Oz^}?uR|b6ol4O8HWx5wbV3EsS`&N`z zx-{t1JpzdasC^sLY~+RsDV(1a`LA>X(b(F?Q`p@Sw|r zK0E4$|>dZ&f-mPOOu7xXEv z{Mu<5;Um-;3bjt_=nWaE!(A8j`HeH%yR1X0H6#iQUOh{qM$kUvbae~IyHh#Cw^}n{DD3lwDmOCw^C)^tJad>-|MQFKU7*gi6jGnl2 zvb%yl2amMy+^*;h`gBEA$_(0wc$R|RlAI97zfN&i&}V5$IY--~GU#&(aUR)aRL##U z0}eYaraK;(I*uVO3i>?%Sp}=h>`mzwK^hL_Wb8K-6k)s5YP#a4pwFSE7Vg}%n};@k z5&Y!nVTXdY?+h~8B7&>7hdttgl|D`WHlF{if~U9b=ZL?}Q7tyG8NS^fUJU%)IV~rX zkvg2q$2b@CF%@3lTg}pv@@V{uw|5A|aNGcDO0js{5zscIeVw0EY^-SDku7EH`YLL^ zA_)sM{U5;bWYIOZW*h=a9aVH3r&juuHZ*b5dpnu4en-C_d|+os;puNPgeY%|aJF-r zoC*3=G#%r~ZC~=IJyo<>P)R*H(6>AOb;1DR(YGYFe9T;$vLL_4u^rO(ES2CjVT6Bc~qP9S9{%#4s-Wj=uW#hK^*gxvyU~It$3CEiJe+N0s5^*u?W@mLv!nCeo-|pyDikSU#E^QW5f672EDi}SOs&Z#< z1|NE90D#K*GdHO6O!&HDL;_WLygD~yQ}8p}2`Nd`@WY26K-d<#tQZmE!)E^gcBdBP0a2nu-3)s^?@k z_Nkq|8USF-%Y~!-t==LO_tb3b-+V!9Tt?OH7YF}FEeMt*i#~W`cCBIhX90VxXfH;O z?D6=G%dHrEu;>oZ!Rh%QCAI3YH9)t^k7D!;0T+zeZm z_ls(N9o; zXRBHge%0Fr9epKY2IwGp#ikz^ru-K$IielA9@(zS-JvYzu%y>W8vxqx1}R^9?^b0g zFf`%<+(i@06UnEIe5`Zkj!5B%0Xj&2p?I|6)yGkC924On3TxsFx%*&FhGWGC95p}( zsmnKAuE--mmeX1y#D`*<4+q<{5eZyjfDU}iH_j9_9_q(Y#E(>EvE8qBq2MY5bdd5w z(f9CjAIe<7J^A9pLxJ~1d8RTq^DU>vbj6hh=pgCEjbl9?W3DK70q&|B6PyZB@>PsK zRLz^=+C1~FHb4hbSm;eI_g*0)H-KIPjBr|0cL;n1ys3u1T0OSyJ - 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") diff --git a/pug/includes/tag-status.pug b/pug/includes/tag-status.pug index 06d6d77..047f594 100644 --- a/pug/includes/tag-status.pug +++ b/pug/includes/tag-status.pug @@ -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 diff --git a/pug/label_grid.pug b/pug/label_grid.pug index 9dd0ef9..07fdf5e 100644 --- a/pug/label_grid.pug +++ b/pug/label_grid.pug @@ -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") diff --git a/pug/tag_grid.pug b/pug/tag_grid.pug index e1f8849..fd2c429 100644 --- a/pug/tag_grid.pug +++ b/pug/tag_grid.pug @@ -1,5 +1,8 @@ extends includes/layout.pug +block title + - title = `Tags | ${title}` + block view script | var items = diff --git a/pug/track_list.pug b/pug/track_list.pug index 8556fe2..1c8cffa 100644 --- a/pug/track_list.pug +++ b/pug/track_list.pug @@ -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") diff --git a/start.js b/start.js index 697ca2c..d850987 100644 --- a/start.js +++ b/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")