Multiple accounts support, load tags online
This commit is contained in:
parent
cd2827791f
commit
26ea869285
14 changed files with 295 additions and 124 deletions
|
@ -1,10 +1,13 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
const {defineEventHandler, getQuery, getValidatedQuery, sendRedirect, createError} = require("h3")
|
||||
const {defineEventHandler, getQuery, getValidatedQuery, sendRedirect, createError, getValidatedRouterParams} = require("h3")
|
||||
const {router, db, sync, select, from} = require("../passthrough")
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
/** @type {import("./load-tags")} */
|
||||
const loadTags = sync.require("./load-tags")
|
||||
|
||||
const displayCurrency = "NZD"
|
||||
const displayCurrencySymbol = "$"
|
||||
const currencyExchange = new Map([
|
||||
|
@ -14,32 +17,38 @@ const currencyExchange = new Map([
|
|||
["EUR", 1.08],
|
||||
["GBP", 1.3],
|
||||
["CAD", 0.7],
|
||||
["NOK", 0.1]
|
||||
["NOK", 0.1],
|
||||
["CHF", 1.13]
|
||||
])
|
||||
|
||||
const sqls = {
|
||||
album_grid: "SELECT item.*, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (item_id) {JOIN TAG} {WHERE} GROUP BY item_id {ORDER}",
|
||||
album_list: "SELECT item.*, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (item_id) {JOIN TAG} {WHERE} GROUP BY item_id ORDER BY band_url, band_name COLLATE NOCASE, item_title COLLATE NOCASE",
|
||||
artist_grid: "SELECT band_name, count(DISTINCT item_id) AS album_count, group_concat(DISTINCT band_url) AS labels, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (item_id) {JOIN TAG} {WHERE} GROUP BY band_name ORDER BY band_name COLLATE NOCASE",
|
||||
artist_list: "SELECT band_name, count(DISTINCT item_id) AS album_count, band_url, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (item_id) {JOIN TAG} {WHERE} GROUP BY band_name ORDER BY band_name COLLATE NOCASE",
|
||||
label_grid: "SELECT iif(count(DISTINCT band_name) = 1, band_name, band_url) AS display_name, band_url, count(DISTINCT item_id) AS album_count, count(DISTINCT band_name) AS artist_count, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (item_id) {JOIN TAG} {WHERE} GROUP BY band_url ORDER BY display_name COLLATE NOCASE",
|
||||
label_list: "SELECT iif(count(DISTINCT band_name) = 1, band_name, band_url) AS display_name, band_url, count(DISTINCT item_id) AS album_count, count(DISTINCT band_name) AS artist_count, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (item_id) {JOIN TAG} {WHERE} GROUP BY band_url ORDER BY display_name COLLATE NOCASE",
|
||||
tag_grid: "SELECT tag, count(*) AS count FROM (SELECT tag, band_url, band_name, item_id, count(*) AS count FROM item_tag INNER JOIN item USING (item_id) GROUP BY tag, band_url) {WHERE} GROUP BY tag ORDER BY count DESC",
|
||||
track_list: "SELECT * FROM track INNER JOIN item USING (item_id) {JOIN TAG} {WHERE} ORDER BY band_url, item_title COLLATE NOCASE, track_number"
|
||||
album_grid: "SELECT item.*, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY item_id {ORDER}",
|
||||
album_list: "SELECT item.*, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY item_id ORDER BY band_url, band_name COLLATE NOCASE, item_title COLLATE NOCASE",
|
||||
artist_grid: "SELECT band_name, count(DISTINCT item_id) AS album_count, group_concat(DISTINCT band_url) AS labels, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY band_name ORDER BY band_name COLLATE NOCASE",
|
||||
artist_list: "SELECT band_name, count(DISTINCT item_id) AS album_count, band_url, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY band_name ORDER BY band_name COLLATE NOCASE",
|
||||
label_grid: "SELECT iif(count(DISTINCT band_name) = 1, band_name, band_url) AS display_name, band_url, count(DISTINCT item_id) AS album_count, count(DISTINCT band_name) AS artist_count, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY band_url ORDER BY display_name COLLATE NOCASE",
|
||||
label_list: "SELECT iif(count(DISTINCT band_name) = 1, band_name, band_url) AS display_name, band_url, count(DISTINCT item_id) AS album_count, count(DISTINCT band_name) AS artist_count, count(*) AS track_count, iif(sum(duration) > 3600, cast(total(duration)/3600 AS INTEGER) || 'h ' || cast(total(duration)/60%60 AS INTEGER) || 'm', cast(total(duration)/60 AS INTEGER) || 'm') AS total_duration FROM item INNER JOIN track USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY band_url ORDER BY display_name COLLATE NOCASE",
|
||||
tag_grid: "SELECT tag, count(*) AS count FROM item_tag INNER JOIN item USING (account, item_id) WHERE account = ? {WHERE} GROUP BY tag, band_url ORDER BY count DESC",
|
||||
track_list: "SELECT * FROM track INNER JOIN item USING (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} ORDER BY band_url, item_title COLLATE NOCASE, track_number"
|
||||
}
|
||||
|
||||
function loadPreviews(locals, field, number, whereClause, filter_field, filter) {
|
||||
const params = [number]
|
||||
let sql = `SELECT ${field}, item_url, item_art_url FROM (SELECT ${field}, item_url, item_art_url, row_number() OVER (PARTITION BY ${field} ORDER BY purchased DESC) AS row_number FROM item {JOIN TAG} {WHERE}) WHERE row_number <= ?`
|
||||
function loadPreviews(locals, field, number, whereClause, account, filter_field, filter) {
|
||||
const params = [account, number]
|
||||
let sql = `SELECT ${field}, item_url, item_art_url FROM (SELECT ${field}, item_url, item_art_url, row_number() OVER (PARTITION BY ${field} ORDER BY purchased DESC) AS row_number FROM item {JOIN TAG} WHERE account = ? {WHERE}) WHERE row_number <= ?`
|
||||
sql = sql.replace("{WHERE}", whereClause)
|
||||
if (whereClause) {
|
||||
params.unshift(filter)
|
||||
if (filter_field === "band_url") {
|
||||
params.splice(1, 0, `%${filter}%`)
|
||||
} else {
|
||||
params.splice(1, 0, filter)
|
||||
}
|
||||
}
|
||||
if (filter_field === "tag" && filter) {
|
||||
sql = sql.replace("{JOIN TAG}", "INNER JOIN item_tag USING (item_id)")
|
||||
sql = sql.replace("{JOIN TAG}", "INNER JOIN item_tag USING (account, item_id)")
|
||||
} else {
|
||||
sql = sql.replace("{JOIN TAG}", "")
|
||||
}
|
||||
|
||||
const previews = db.prepare(sql).all(params)
|
||||
// TODO: performance?
|
||||
for (const item of locals.items) {
|
||||
|
@ -52,20 +61,26 @@ function loadPreviews(locals, field, number, whereClause, filter_field, filter)
|
|||
}
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
arrange: z.enum(["album", "artist", "label", "tag", "track"]),
|
||||
shape: z.enum(["grid", "list"]),
|
||||
filter_field: z.enum(["band_name", "band_url", "item_id", "tag"]).optional(),
|
||||
filter: z.string().optional()
|
||||
})
|
||||
const schema = {
|
||||
query: z.object({
|
||||
arrange: z.enum(["album", "artist", "label", "tag", "track"]),
|
||||
shape: z.enum(["grid", "list"]),
|
||||
filter_field: z.enum(["band_name", "band_url", "item_id", "tag"]).optional(),
|
||||
filter: z.string().optional()
|
||||
}),
|
||||
params: z.object({
|
||||
account: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
router.get("/", defineEventHandler({
|
||||
router.get("/:account/", defineEventHandler({
|
||||
onBeforeResponse: pugSync.compressResponse,
|
||||
handler: async event => {
|
||||
try {
|
||||
var {arrange, shape, filter, filter_field} = await getValidatedQuery(event, schema.parse)
|
||||
var {account} = await getValidatedRouterParams(event, schema.params.parse)
|
||||
var {arrange, shape, filter, filter_field} = await getValidatedQuery(event, schema.query.parse)
|
||||
} catch (e) {
|
||||
return sendRedirect(event, "/?arrange=album&shape=grid", 302)
|
||||
return sendRedirect(event, "?arrange=album&shape=grid", 302)
|
||||
}
|
||||
const query = getQuery(event)
|
||||
if (arrange === "track") {
|
||||
|
@ -73,7 +88,7 @@ router.get("/", defineEventHandler({
|
|||
query.shape = "list"
|
||||
}
|
||||
const mode = `${arrange}_${shape}`
|
||||
const params = []
|
||||
const params = [account]
|
||||
let sql = sqls[mode]
|
||||
let whereClause = ""
|
||||
if (filter_field && filter) {
|
||||
|
@ -84,23 +99,33 @@ router.get("/", defineEventHandler({
|
|||
} else {
|
||||
params.push(filter)
|
||||
}
|
||||
whereClause = `WHERE ${filter_field} ${operator} ?`
|
||||
whereClause = `AND ${filter_field} ${operator} ?`
|
||||
sql = sql.replace("{ORDER}", "ORDER BY item_title COLLATE NOCASE")
|
||||
} else {
|
||||
sql = sql.replace("{ORDER}", "ORDER BY purchased DESC")
|
||||
}
|
||||
sql = sql.replace("{WHERE}", whereClause)
|
||||
if (filter_field === "tag" && filter) {
|
||||
sql = sql.replace("{JOIN TAG}", "INNER JOIN item_tag USING (item_id)")
|
||||
sql = sql.replace("{JOIN TAG}", "INNER JOIN item_tag USING (account, item_id)")
|
||||
} else {
|
||||
sql = sql.replace("{JOIN TAG}", "")
|
||||
}
|
||||
const prepared = db.prepare(sql)
|
||||
if (arrange === "tag") {
|
||||
prepared.raw()
|
||||
if (mode === "tag_grid" && ((!filter_field || !filter) || filter_field === "tag")) {
|
||||
sql = `SELECT tag, count(*) AS count FROM (${sql}) GROUP BY tag ORDER BY count DESC`
|
||||
}
|
||||
try {
|
||||
const prepared = db.prepare(sql)
|
||||
if (arrange === "tag") {
|
||||
prepared.raw()
|
||||
}
|
||||
var items = prepared.all(params)
|
||||
} catch (e) {
|
||||
console.error(sql, params)
|
||||
throw e
|
||||
}
|
||||
const locals = {
|
||||
items: prepared.all(params),
|
||||
items,
|
||||
account,
|
||||
query,
|
||||
count: {
|
||||
total: db.prepare("SELECT count(*) FROM item").pluck().get(),
|
||||
|
@ -123,9 +148,12 @@ router.get("/", defineEventHandler({
|
|||
}
|
||||
}
|
||||
if (mode === "artist_grid") {
|
||||
loadPreviews(locals, "band_name", 4, whereClause, filter_field, filter)
|
||||
loadPreviews(locals, "band_name", 4, whereClause, account, filter_field, filter)
|
||||
} else if (mode === "label_grid") {
|
||||
loadPreviews(locals, "band_url", 6, whereClause, filter_field, filter)
|
||||
loadPreviews(locals, "band_url", 6, whereClause, account, filter_field, filter)
|
||||
} else if (arrange === "tag") {
|
||||
locals.downloadManager = loadTags.downloadManager
|
||||
locals.downloader = loadTags.downloadManager.check(account)
|
||||
}
|
||||
return pugSync.render(event, `${arrange}_${shape}.pug`, locals)
|
||||
}
|
||||
|
|
99
routes/load-tags.js
Normal file
99
routes/load-tags.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
// @ts-check
|
||||
|
||||
const domino = require("domino")
|
||||
const {z} = require("zod")
|
||||
const {getValidatedQuery, readValidatedBody, defineEventHandler} = require("h3")
|
||||
|
||||
const {sync, db, from, router} = require("../passthrough")
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
const i = db.prepare("INSERT OR IGNORE INTO item_tag (account, item_id, tag) VALUES (?, ?, ?)")
|
||||
|
||||
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 FROM item LEFT JOIN item_tag USING (account, item_id) WHERE account = ? AND tag IS NULL").all(account)
|
||||
this.total = this.untaggedItems.length
|
||||
this.running = false
|
||||
this.outcome = null
|
||||
}
|
||||
|
||||
async _start() {
|
||||
if (this.running) return
|
||||
this.running = true
|
||||
try {
|
||||
for (const {account, item_id, item_title, item_url} of this.untaggedItems) {
|
||||
const html = await fetch(item_url).then(res => res.text())
|
||||
const doc = domino.createDocument(html)
|
||||
// @ts-ignore
|
||||
const tags = [...doc.querySelectorAll(".tag").cache].map(e => e.textContent)
|
||||
db.transaction(() => {
|
||||
for (const tag of tags) {
|
||||
i.run(account, item_id, tag)
|
||||
}
|
||||
})()
|
||||
this.processed++
|
||||
}
|
||||
this.outcome = "Success"
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.outcome = e.toString()
|
||||
} finally {
|
||||
this.running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const downloadManager = new class {
|
||||
/** @type {Map<string, TagDownloader>} */
|
||||
inProgressTagDownloads = sync.remember(() => new Map())
|
||||
|
||||
/** @param {string} account */
|
||||
check(account) {
|
||||
return this.inProgressTagDownloads.get(account) || (() => {
|
||||
const downloader = new TagDownloader(account)
|
||||
this.inProgressTagDownloads.set(account, downloader)
|
||||
console.log(`created downloader ${account}`)
|
||||
setTimeout(() => {
|
||||
this.resolve(account)
|
||||
})
|
||||
return downloader
|
||||
})()
|
||||
}
|
||||
|
||||
/** @param {string} account */
|
||||
start(account) {
|
||||
const downloader = this.check(account)
|
||||
downloader._start()
|
||||
return downloader
|
||||
}
|
||||
|
||||
/** @param {string} account */
|
||||
resolve(account) {
|
||||
const downloader = this.check(account)
|
||||
if (!downloader.running) {
|
||||
console.log(`disposed downloader ${account}`)
|
||||
this.inProgressTagDownloads.delete(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
account: z.string()
|
||||
})
|
||||
|
||||
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})
|
||||
}))
|
||||
|
||||
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})
|
||||
}))
|
||||
|
||||
module.exports.downloadManager = downloadManager
|
Loading…
Add table
Add a link
Reference in a new issue