Simple mobile support
This commit is contained in:
parent
924c7395cf
commit
852a053e2b
17 changed files with 207 additions and 93 deletions
|
@ -18,7 +18,7 @@ sync.require("./settings")
|
|||
|
||||
// Files
|
||||
|
||||
router.get("/static/stacks.min.css", defineEventHandler({
|
||||
router.get("/static/stacks.css", defineEventHandler({
|
||||
onBeforeResponse: pugSync.compressResponse,
|
||||
handler: async event => {
|
||||
handleCacheHeaders(event, {maxAge: 86400})
|
||||
|
@ -65,7 +65,7 @@ router.get("/static/player-marker.js", defineEventHandler({
|
|||
router.get("/favicon.png", defineEventHandler({
|
||||
handler: async event => {
|
||||
handleCacheHeaders(event, {maxAge: 86400})
|
||||
defaultContentType(event, "text/javascript")
|
||||
defaultContentType(event, "image/png")
|
||||
return fs.promises.readFile("public/favicon.png")
|
||||
}
|
||||
}))
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
const {defineEventHandler, getQuery, getValidatedQuery, sendRedirect, createError, getValidatedRouterParams, getCookie} = require("h3")
|
||||
const {router, db, sync, select, from} = require("../passthrough")
|
||||
const {router, db, sync} = require("../passthrough")
|
||||
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
/** @type {import("./load-tags")} */
|
||||
const loadTags = sync.require("./load-tags")
|
||||
/** @type {import("./schema")} */
|
||||
const schema = sync.require("./schema")
|
||||
|
||||
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 (account, item_id) {JOIN TAG} WHERE account = ? {WHERE} GROUP BY item_id {ORDER}",
|
||||
|
@ -24,7 +23,7 @@ const sqls = {
|
|||
|
||||
function loadPreviews(locals, field, number, whereClause, account, filter_field, filter, filter_fuzzy) {
|
||||
const params = [account, number]
|
||||
let sql = `SELECT ${field}, item_id, item_type, item_url, item_art_url FROM (SELECT ${field}, item_id, item_type, 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 <= ?`
|
||||
let sql = `SELECT ${field}, item_id, item_title, item_type, item_url, item_art_url FROM (SELECT ${field}, item_title, item_id, item_type, 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) {
|
||||
if (filter_field === "band_url" || filter_fuzzy) {
|
||||
|
@ -69,25 +68,12 @@ pugSync.addGlobals({
|
|||
}
|
||||
})
|
||||
|
||||
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_title", "item_id", "tag", "why"]).optional(),
|
||||
filter: z.string().optional(),
|
||||
filter_fuzzy: z.enum(["true"]).optional()
|
||||
}),
|
||||
params: z.object({
|
||||
account: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
router.get("/:account/", defineEventHandler({
|
||||
onBeforeResponse: pugSync.compressResponse,
|
||||
handler: async event => {
|
||||
const {account} = await getValidatedRouterParams(event, schema.schema.account.parse)
|
||||
try {
|
||||
var {account} = await getValidatedRouterParams(event, schema.params.parse)
|
||||
var {arrange, shape, filter, filter_field, filter_fuzzy} = await getValidatedQuery(event, schema.query.parse)
|
||||
var {arrange, shape, filter, filter_field, filter_fuzzy} = await getValidatedQuery(event, schema.schema.appQuery.parse)
|
||||
if (filter_field === "why" && arrange !== "album") throw new Error("filter not compatible with arrangement")
|
||||
} catch (e) {
|
||||
return sendRedirect(event, "?arrange=album&shape=grid", 302)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// @ts-check
|
||||
|
||||
const {getCookie, defineEventHandler, readValidatedBody, setCookie} = require("h3")
|
||||
const {z} = require("zod")
|
||||
const {getCookie, defineEventHandler, readValidatedBody, setCookie, getValidatedRouterParams} = require("h3")
|
||||
const {sync, select, db, router} = require("../passthrough")
|
||||
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
/** @type {import("./schema")} */
|
||||
const schema = sync.require("./schema")
|
||||
|
||||
const currencyExchange = new Map([
|
||||
["AUD", 0.63],
|
||||
["BRL", 0.17],
|
||||
|
@ -21,6 +23,12 @@ const currencyExchange = new Map([
|
|||
])
|
||||
const currencies = [...currencyExchange.keys()]
|
||||
|
||||
pugSync.beforeInclude("includes/layout.pug", async (from, event, locals) => {
|
||||
return {
|
||||
currencies
|
||||
}
|
||||
})
|
||||
|
||||
pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {account, currency}) => {
|
||||
let displayCurrency = currency || getCookie(event, "bcex-currency") || ""
|
||||
if (!currencyExchange.has(displayCurrency)) displayCurrency = "NZD"
|
||||
|
@ -44,20 +52,17 @@ pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {acco
|
|||
}).reduce((a, c) => a + c, 0)) * currencyRoundTo,
|
||||
displayCurrency,
|
||||
taste: db.prepare("with popularity (a) as (select avg(also_collected_count) from item WHERE account = ? group by band_url) select sum(iif(a >= 0 and a < 20, 1, 0)) as cold, sum(iif(a >= 20 and a < 200, 1, 0)) as warm, sum(iif(a >= 200 and a < 2000, 1, 0)) as hot, sum(iif(a >= 2000, 1, 0)) as supernova from popularity").raw().get(account)
|
||||
},
|
||||
currencies
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const schema = {
|
||||
currency: z.object({
|
||||
currency: z.string().regex(/^[A-Z]{3}$/),
|
||||
account: z.string()
|
||||
})
|
||||
}
|
||||
router.get("/:account/collection-stats", defineEventHandler(async event => {
|
||||
const {account} = await getValidatedRouterParams(event, schema.schema.account.parse)
|
||||
return pugSync.render(event, "collection-stats.pug", {account, isStatsPage: true})
|
||||
}))
|
||||
|
||||
router.post("/api/settings/currency", defineEventHandler(async event => {
|
||||
const {currency, account} = await readValidatedBody(event, schema.currency.parse)
|
||||
const {currency, account} = await readValidatedBody(event, schema.schema.postCurrency.parse)
|
||||
setCookie(event, "bcex-currency", currency)
|
||||
return pugSync.render(event, "includes/collection-stats.pug", {account, currency})
|
||||
}))
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert/strict")
|
||||
const fs = require("fs")
|
||||
const sqlite = require("better-sqlite3")
|
||||
const domino = require("domino")
|
||||
const {defineEventHandler, readValidatedBody, setCookie, getCookie} = require("h3")
|
||||
const {z} = require("zod")
|
||||
|
||||
const {sync, db, router} = require("../passthrough")
|
||||
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
/** @type {import("./load-tags")} */
|
||||
const loadTags = sync.require("./load-tags")
|
||||
|
||||
/** @type {import("./schema")} */
|
||||
const schema = sync.require("./schema")
|
||||
|
||||
async function loadCollection(inputUsername) {
|
||||
assert.match(inputUsername, /^[a-z0-9_-]+$/)
|
||||
const html = await fetch(`https://bandcamp.com/${inputUsername}`).then(res => res.text())
|
||||
const doc = domino.createDocument(html)
|
||||
|
||||
const first = doc.querySelector(".collection-item-container")
|
||||
const first = doc.querySelector(".collection-item-container[data-token]")
|
||||
assert(first)
|
||||
const token = first.getAttribute("data-token")
|
||||
assert(token)
|
||||
|
@ -49,6 +52,7 @@ async function loadCollection(inputUsername) {
|
|||
const preparedItem = db.prepare(`INSERT INTO item (${columns.join(", ")}) VALUES (${columns.map(x => "@" + x).join(", ")}) ON CONFLICT DO UPDATE SET ${upsert_columns.map(x => `${x} = @${x}`).join(", ")}`)
|
||||
db.transaction(() => {
|
||||
for (const item of items.items) {
|
||||
if (!item.tralbum_type.match(/[at]/)) continue // p=product and s=subscription not supported
|
||||
preparedItem.run({
|
||||
account,
|
||||
...item,
|
||||
|
@ -88,12 +92,8 @@ async function loadCollection(inputUsername) {
|
|||
}
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
account: z.string()
|
||||
})
|
||||
|
||||
router.post("/api/load-collection", defineEventHandler(async event => {
|
||||
const {account} = await readValidatedBody(event, schema.parse)
|
||||
const {account} = await readValidatedBody(event, schema.schema.account.parse)
|
||||
const result = await loadCollection(account)
|
||||
setCookie(event, "accounts", (getCookie(event, "accounts") || "").split("|").concat(account).join("|"))
|
||||
return pugSync.render(event, "collection-loaded.pug", result)
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
// @ts-check
|
||||
|
||||
const domino = require("domino")
|
||||
const {z} = require("zod")
|
||||
const {getValidatedQuery, readValidatedBody, defineEventHandler} = require("h3")
|
||||
|
||||
const {sync, db, from, router} = require("../passthrough")
|
||||
const {sync, db, router} = require("../passthrough")
|
||||
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
/** @type {import("./schema")} */
|
||||
const schema = sync.require("./schema")
|
||||
|
||||
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)")
|
||||
|
||||
|
@ -118,17 +122,13 @@ const downloadManager = new class {
|
|||
}
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
account: z.string()
|
||||
})
|
||||
|
||||
router.get("/api/tag-download", defineEventHandler(async event => {
|
||||
const {account} = await getValidatedQuery(event, schema.parse)
|
||||
const {account} = await getValidatedQuery(event, schema.schema.account.parse)
|
||||
return pugSync.render(event, "includes/tag-status.pug", {account})
|
||||
}))
|
||||
|
||||
router.post("/api/tag-download", defineEventHandler(async event => {
|
||||
const {account} = await readValidatedBody(event, schema.parse)
|
||||
const {account} = await readValidatedBody(event, schema.schema.account.parse)
|
||||
downloadManager.start(account)
|
||||
return pugSync.render(event, "includes/tag-status.pug", {account})
|
||||
}))
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
const {sync, router} = require("../passthrough")
|
||||
const {defineEventHandler} = require("h3")
|
||||
const {getValidatedRouterParams} = require("h3")
|
||||
|
@ -8,14 +7,11 @@ const {getValidatedRouterParams} = require("h3")
|
|||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
|
||||
const schema = z.object({
|
||||
item_type: z.enum(["album", "track"]),
|
||||
item_id: z.number({coerce: true}),
|
||||
track_id: z.number({coerce: true}).optional()
|
||||
})
|
||||
/** @type {import("./schema")} */
|
||||
const schema = sync.require("./schema")
|
||||
|
||||
const play = defineEventHandler(async event => {
|
||||
const locals = await getValidatedRouterParams(event, schema.parse)
|
||||
const locals = await getValidatedRouterParams(event, schema.schema.play.parse)
|
||||
return pugSync.render(event, "player.pug", locals)
|
||||
})
|
||||
|
||||
|
|
30
routes/schema.js
Normal file
30
routes/schema.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
|
||||
const schema = {
|
||||
appQuery: z.object({
|
||||
arrange: z.enum(["album", "artist", "label", "tag", "track"]),
|
||||
shape: z.enum(["grid", "list"]),
|
||||
filter_field: z.enum(["band_name", "band_url", "item_title", "item_id", "tag", "why"]).optional(),
|
||||
filter: z.string().optional(),
|
||||
filter_fuzzy: z.enum(["true"]).optional()
|
||||
}),
|
||||
account: z.object({
|
||||
account: z.string().regex(/^[a-z0-9_-]+$/)
|
||||
}),
|
||||
postCurrency: z.object({
|
||||
currency: z.string().regex(/^[A-Z]{3}$/),
|
||||
account: z.string()
|
||||
}),
|
||||
play: z.object({
|
||||
item_type: z.enum(["album", "track"]),
|
||||
item_id: z.number({coerce: true}),
|
||||
track_id: z.number({coerce: true}).optional()
|
||||
}),
|
||||
inlinePlayer: z.object({
|
||||
inline_player: z.string().optional()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.schema = schema
|
|
@ -1,17 +1,13 @@
|
|||
// @ts-check
|
||||
|
||||
const {z} = require("zod")
|
||||
const {router} = require("../passthrough")
|
||||
const {sync, router} = require("../passthrough")
|
||||
const {defineEventHandler, readValidatedBody, setCookie, setResponseHeader} = require("h3")
|
||||
|
||||
const schema = {
|
||||
inline_player: z.object({
|
||||
inline_player: z.string().optional()
|
||||
})
|
||||
}
|
||||
/** @type {import("./schema")} */
|
||||
const schema = sync.require("./schema")
|
||||
|
||||
router.post("/api/settings/inline-player", defineEventHandler(async event => {
|
||||
const {inline_player} = await readValidatedBody(event, schema.inline_player.parse)
|
||||
const {inline_player} = await readValidatedBody(event, schema.schema.inlinePlayer.parse)
|
||||
setCookie(event, "bcex-inline-player-disabled", String(!inline_player))
|
||||
setResponseHeader(event, "HX-Refresh", "true")
|
||||
return null
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue