Refactor collection stats to beforeInclude
This commit is contained in:
		
							parent
							
								
									8b8eb09372
								
							
						
					
					
						commit
						5cda6a1687
					
				
					 7 changed files with 166 additions and 108 deletions
				
			
		
							
								
								
									
										42
									
								
								pug-sync.js
									
										
									
									
									
								
							
							
						
						
									
										42
									
								
								pug-sync.js
									
										
									
									
									
								
							| 
						 | 
					@ -5,7 +5,7 @@ const fs = require("fs")
 | 
				
			||||||
const {join} = require("path")
 | 
					const {join} = require("path")
 | 
				
			||||||
const h3 = require("h3")
 | 
					const h3 = require("h3")
 | 
				
			||||||
const {defineEventHandler, defaultContentType, setResponseStatus, getQuery, getRequestHeader, setResponseHeader} = h3
 | 
					const {defineEventHandler, defaultContentType, setResponseStatus, getQuery, getRequestHeader, setResponseHeader} = h3
 | 
				
			||||||
const {compileFile} = require("@cloudrac3r/pug")
 | 
					const pug = require("@cloudrac3r/pug")
 | 
				
			||||||
const pretty = process.argv.join(" ").includes("test")
 | 
					const pretty = process.argv.join(" ").includes("test")
 | 
				
			||||||
const {sync} = require("./passthrough")
 | 
					const {sync} = require("./passthrough")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,34 @@ function addGlobals(obj) {
 | 
				
			||||||
	Object.assign(globals, obj)
 | 
						Object.assign(globals, obj)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {Map<string, (from: string, event: import("h3").H3Event, locals: Record<string, any>) => Promise<string>>} */
 | 
				
			||||||
 | 
					const beforeIncludes = new Map()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * So the idea with this one is that because I'm using htmx I want to be able to render certain page fragments on their own.
 | 
				
			||||||
 | 
					 * Like if I change the currency, I should re-render just the collection stats because that's where the currency display is.
 | 
				
			||||||
 | 
					 * So I do this by separating the stats template into its own file, then the main page can include the stats template. Easy.
 | 
				
			||||||
 | 
					 * The template can be a mixin or it can just be the thing. If it's a mixin, I need another mini-template to include it, ok.
 | 
				
			||||||
 | 
					 * This requires setting up the mini-endpoint as well just for the template (or mini-template), ok. The gross part is that both
 | 
				
			||||||
 | 
					 * the mini-endpoint and main endpoint need to gather the variables, like collection stats from the database, and give them to the template. Hmm.
 | 
				
			||||||
 | 
					 * Even if that variable getter is in its own isolated function, I think it feels bad to have to manually call that in every endpoint whose page
 | 
				
			||||||
 | 
					 * happens to eventually include collection stats. It has bad separation of concerns, and scales badly as more templates get split out this way.
 | 
				
			||||||
 | 
					 * The collection stats template ALWAYS needs its same variables in the same way, no matter which page it happens to be included in.
 | 
				
			||||||
 | 
					 * So here's how this is going to work. You can define a function through pugSync that will be called automatically during render if
 | 
				
			||||||
 | 
					 * the main page happens to include the relevant template. We get the dependencies through pug.compileClientWithDependenciesTracked.
 | 
				
			||||||
 | 
					 * @example ```js
 | 
				
			||||||
 | 
					beforeInclude("pug/includes/collection-stats.pug", (from, event, locals) => {
 | 
				
			||||||
 | 
					locals.stats = {
 | 
				
			||||||
 | 
					total: db.prepare(...
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					 * @param {string} filename
 | 
				
			||||||
 | 
					 * @param {(from: string, event: import("h3").H3Event, locals: Record<string, any>) => Promise<any>} getLocals
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function beforeInclude(filename, getLocals) {
 | 
				
			||||||
 | 
						const path = join(__dirname, "pug", filename)
 | 
				
			||||||
 | 
						beforeIncludes.set(path, getLocals)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function and(event, params) {
 | 
					function and(event, params) {
 | 
				
			||||||
	const newParams = Object.assign(getQuery(event), params)
 | 
						const newParams = Object.assign(getQuery(event), params)
 | 
				
			||||||
	Object.keys(newParams).forEach(key => !newParams[key] && delete newParams[key])
 | 
						Object.keys(newParams).forEach(key => !newParams[key] && delete newParams[key])
 | 
				
			||||||
| 
						 | 
					@ -36,9 +64,18 @@ function render(event, filename, locals) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function compile() {
 | 
						function compile() {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			const template = compileFile(path, {pretty})
 | 
								const content = fs.readFileSync(path, "utf8")
 | 
				
			||||||
 | 
								const template = pug.compile(content, {filename: path, pretty})
 | 
				
			||||||
 | 
								const dependencies = pug.compileClientWithDependenciesTracked(content, {filename: path, pretty}).dependencies.concat(path)
 | 
				
			||||||
			pugCache.set(path, async (event, locals) => {
 | 
								pugCache.set(path, async (event, locals) => {
 | 
				
			||||||
				const localAnd = params => and(event, params)
 | 
									const localAnd = params => and(event, params)
 | 
				
			||||||
 | 
									// automatically generate locals required by included templates
 | 
				
			||||||
 | 
									console.log([...beforeIncludes.keys()])
 | 
				
			||||||
 | 
									for (const dep of dependencies) {
 | 
				
			||||||
 | 
										console.log(dep)
 | 
				
			||||||
 | 
										const fn = beforeIncludes.get(dep)
 | 
				
			||||||
 | 
										if (fn) Object.assign(locals, await fn(path, event, locals))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				defaultContentType(event, "text/html; charset=utf-8")
 | 
									defaultContentType(event, "text/html; charset=utf-8")
 | 
				
			||||||
				return template(Object.assign({},
 | 
									return template(Object.assign({},
 | 
				
			||||||
					getQuery(event), // Query parameters can be easily accessed on the top level but don't allow them to overwrite anything
 | 
										getQuery(event), // Query parameters can be easily accessed on the top level but don't allow them to overwrite anything
 | 
				
			||||||
| 
						 | 
					@ -95,3 +132,4 @@ module.exports.render = render
 | 
				
			||||||
module.exports.createRoute = createRoute
 | 
					module.exports.createRoute = createRoute
 | 
				
			||||||
module.exports.compressResponse = compressResponse
 | 
					module.exports.compressResponse = compressResponse
 | 
				
			||||||
module.exports.and = and
 | 
					module.exports.and = and
 | 
				
			||||||
 | 
					module.exports.beforeInclude = beforeInclude
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										58
									
								
								pug/includes/collection-stats.pug
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								pug/includes/collection-stats.pug
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,58 @@
 | 
				
			||||||
 | 
					#collection-stats.s-sidebarwidget
 | 
				
			||||||
 | 
					  .s-sidebarwidget--header Collection
 | 
				
			||||||
 | 
					    .s-sidebarwidget--action
 | 
				
			||||||
 | 
					      input(type="hidden" name="account" value=account)
 | 
				
			||||||
 | 
					      button.s-link(hx-post="/api/load-collection" hx-target="#collection-sync" hx-select="#collection-sync" hx-include="previous input") Sync
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  table.s-sidebarwidget--content.s-sidebarwidget__items
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th items
 | 
				
			||||||
 | 
					      td= count.total
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th runtime
 | 
				
			||||||
 | 
					      td= count.runtime
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th format
 | 
				
			||||||
 | 
					      td
 | 
				
			||||||
 | 
					        = count.albums
 | 
				
			||||||
 | 
					        span.fc-black-400= ` albums`
 | 
				
			||||||
 | 
					        span.fc-black-250= ` / `
 | 
				
			||||||
 | 
					        = count.singles
 | 
				
			||||||
 | 
					        span.fc-black-400= ` singles`
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th price
 | 
				
			||||||
 | 
					      td
 | 
				
			||||||
 | 
					        = count.free
 | 
				
			||||||
 | 
					        span.fc-black-400= ` free`
 | 
				
			||||||
 | 
					        span.fc-black-250= ` / `
 | 
				
			||||||
 | 
					        = count.paid
 | 
				
			||||||
 | 
					        span.fc-black-400= ` paid`
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th tracks
 | 
				
			||||||
 | 
					      td
 | 
				
			||||||
 | 
					        = count.tracks
 | 
				
			||||||
 | 
					        span.pl8.fc-black-250= ` / `
 | 
				
			||||||
 | 
					        span.fc-black-400 avg #{count.avgTracks}
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th tags
 | 
				
			||||||
 | 
					      td
 | 
				
			||||||
 | 
					        = count.tags
 | 
				
			||||||
 | 
					        span.pl8.fc-black-250= ` / `
 | 
				
			||||||
 | 
					        span.fc-black-400 avg #{count.avgTags}
 | 
				
			||||||
 | 
					        span.fc-black-250= ` / `
 | 
				
			||||||
 | 
					        span.fc-black-400 lonely #{count.lonelyTags}
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th value
 | 
				
			||||||
 | 
					      td
 | 
				
			||||||
 | 
					        = `${count.value} `
 | 
				
			||||||
 | 
					        span.fc-black-400 #{count.displayCurrency}
 | 
				
			||||||
 | 
					    tr.s-sidebarwidget--item
 | 
				
			||||||
 | 
					      th diversity
 | 
				
			||||||
 | 
					      //- supernova red-500, warm yellow-500, hot orange-500
 | 
				
			||||||
 | 
					      //- 0-9 black, 10-99 yellow, 100-999 orange, 1000+ red
 | 
				
			||||||
 | 
					      td.w100
 | 
				
			||||||
 | 
					        .s-progress.d-grid.g2.h4.mtn6(style=`grid-template-columns: ${count.taste.map(t => t + "fr").join(" ")}`).bg-white.fc-black-400.fs-fine.lh-xxl
 | 
				
			||||||
 | 
					          .s-progress--bar.bg-black-400(title=`${count.taste[0]} labels with <20 fans`)= count.taste[0]
 | 
				
			||||||
 | 
					          .s-progress--bar.bg-yellow-400(title=`${count.taste[1]} labels with 20-199 fans`)= count.taste[1]
 | 
				
			||||||
 | 
					          .s-progress--bar.bg-orange-400(title=`${count.taste[2]} labels with 200-1999 fans`)= count.taste[2]
 | 
				
			||||||
 | 
					          .s-progress--bar.bg-red-400(title=`${count.taste[3]} labels with >2000 fans`)= count.taste[3]
 | 
				
			||||||
| 
						 | 
					@ -103,64 +103,7 @@ html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            #collection-sync.d-none
 | 
					            #collection-sync.d-none
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            #collection-stats.s-sidebarwidget
 | 
					            include collection-stats.pug
 | 
				
			||||||
              .s-sidebarwidget--header Collection
 | 
					 | 
				
			||||||
                .s-sidebarwidget--action
 | 
					 | 
				
			||||||
                  input(type="hidden" name="account" value=account)
 | 
					 | 
				
			||||||
                  button.s-link(hx-post="/api/load-collection" hx-target="#collection-sync" hx-select="#collection-sync" hx-include="previous input") Sync
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              table.s-sidebarwidget--content.s-sidebarwidget__items
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th items
 | 
					 | 
				
			||||||
                  td= count.total
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th runtime
 | 
					 | 
				
			||||||
                  td= count.runtime
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th format
 | 
					 | 
				
			||||||
                  td
 | 
					 | 
				
			||||||
                    = count.albums
 | 
					 | 
				
			||||||
                    span.fc-black-400= ` albums`
 | 
					 | 
				
			||||||
                    span.fc-black-250= ` / `
 | 
					 | 
				
			||||||
                    = count.singles
 | 
					 | 
				
			||||||
                    span.fc-black-400= ` singles`
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th price
 | 
					 | 
				
			||||||
                  td
 | 
					 | 
				
			||||||
                    = count.free
 | 
					 | 
				
			||||||
                    span.fc-black-400= ` free`
 | 
					 | 
				
			||||||
                    span.fc-black-250= ` / `
 | 
					 | 
				
			||||||
                    = count.paid
 | 
					 | 
				
			||||||
                    span.fc-black-400= ` paid`
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th tracks
 | 
					 | 
				
			||||||
                  td
 | 
					 | 
				
			||||||
                    = count.tracks
 | 
					 | 
				
			||||||
                    span.pl8.fc-black-250= ` / `
 | 
					 | 
				
			||||||
                    span.fc-black-400 avg #{count.avgTracks}
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th tags
 | 
					 | 
				
			||||||
                  td
 | 
					 | 
				
			||||||
                    = count.tags
 | 
					 | 
				
			||||||
                    span.pl8.fc-black-250= ` / `
 | 
					 | 
				
			||||||
                    span.fc-black-400 avg #{count.avgTags}
 | 
					 | 
				
			||||||
                    span.fc-black-250= ` / `
 | 
					 | 
				
			||||||
                    span.fc-black-400 lonely #{count.lonelyTags}
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th value
 | 
					 | 
				
			||||||
                  td
 | 
					 | 
				
			||||||
                    = `${count.value} `
 | 
					 | 
				
			||||||
                    span.fc-black-400 #{count.displayCurrency}
 | 
					 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					 | 
				
			||||||
                  th diversity
 | 
					 | 
				
			||||||
                  //- supernova red-500, warm yellow-500, hot orange-500
 | 
					 | 
				
			||||||
                  //- 0-9 black, 10-99 yellow, 100-999 orange, 1000+ red
 | 
					 | 
				
			||||||
                  td.w100
 | 
					 | 
				
			||||||
                    .s-progress.d-grid.g2.h4.mtn6(style=`grid-template-columns: ${count.taste.map(t => t + "fr").join(" ")}`).bg-white.fc-black-400.fs-fine.lh-xxl
 | 
					 | 
				
			||||||
                      .s-progress--bar.bg-black-400(title=`${count.taste[0]} labels with <20 fans`)= count.taste[0]
 | 
					 | 
				
			||||||
                      .s-progress--bar.bg-yellow-400(title=`${count.taste[1]} labels with 20-199 fans`)= count.taste[1]
 | 
					 | 
				
			||||||
                      .s-progress--bar.bg-orange-400(title=`${count.taste[2]} labels with 200-1999 fans`)= count.taste[2]
 | 
					 | 
				
			||||||
                      .s-progress--bar.bg-red-400(title=`${count.taste[3]} labels with >2000 fans`)= count.taste[3]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        main.fl-grow1
 | 
					        main.fl-grow1
 | 
				
			||||||
          block view
 | 
					          block view
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,19 +8,6 @@ const pugSync = sync.require("../pug-sync")
 | 
				
			||||||
/** @type {import("./load-tags")} */
 | 
					/** @type {import("./load-tags")} */
 | 
				
			||||||
const loadTags = sync.require("./load-tags")
 | 
					const loadTags = sync.require("./load-tags")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const currencyExchange = new Map([
 | 
					 | 
				
			||||||
	["AUD", 0.63],
 | 
					 | 
				
			||||||
	["BRL", 0.17],
 | 
					 | 
				
			||||||
	["CAD", 0.7],
 | 
					 | 
				
			||||||
	["CHF", 1.13],
 | 
					 | 
				
			||||||
	["EUR", 1.08],
 | 
					 | 
				
			||||||
	["GBP", 1.3],
 | 
					 | 
				
			||||||
	["JPY", 0.0067],
 | 
					 | 
				
			||||||
	["NOK", 0.1],
 | 
					 | 
				
			||||||
	["NZD", 0.57],
 | 
					 | 
				
			||||||
	["USD", 1],
 | 
					 | 
				
			||||||
])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const sqls = {
 | 
					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}",
 | 
						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",
 | 
						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",
 | 
				
			||||||
| 
						 | 
					@ -150,33 +137,10 @@ router.get("/:account/", defineEventHandler({
 | 
				
			||||||
			throw e
 | 
								throw e
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let displayCurrency = getCookie(event, "bcex-currency") || ""
 | 
					 | 
				
			||||||
		if (!currencyExchange.has(displayCurrency)) displayCurrency = "NZD"
 | 
					 | 
				
			||||||
		const currencyRoundTo = displayCurrency === "JPY" ? 1000 : 10
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const locals = {
 | 
							const locals = {
 | 
				
			||||||
			items,
 | 
								items,
 | 
				
			||||||
			account,
 | 
								account,
 | 
				
			||||||
			query,
 | 
								query,
 | 
				
			||||||
			count: {
 | 
					 | 
				
			||||||
				total: db.prepare("SELECT count(*) FROM item WHERE account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				runtime: db.prepare("SELECT iif(sum(duration) > 86400, cast(total(duration)/86400 AS INTEGER) || 'd ' || cast(total(duration)/3600%24 AS INTEGER) || 'h', cast(total(duration)/3600 AS INTEGER) || 'h') FROM track WHERE account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				albums: db.prepare("SELECT count(*) FROM item WHERE item_type = 'album' AND account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				singles: db.prepare("SELECT count(*) FROM item WHERE item_type = 'track' AND account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				free: db.prepare("SELECT count(*) FROM item WHERE price = 0 AND account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				paid: db.prepare("SELECT count(*) FROM item WHERE price > 0 AND account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				tracks: db.prepare("SELECT count(*) FROM track WHERE account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				avgTracks: Math.round(db.prepare("SELECT avg(count) FROM (SELECT count(*) AS count FROM track INNER JOIN item USING (account, item_id) WHERE item_type = 'album' AND account = ? GROUP BY item_id)").pluck().get(account)*10)/10,
 | 
					 | 
				
			||||||
				tags: db.prepare("SELECT count(*) FROM item_tag WHERE account = ?").pluck().get(account),
 | 
					 | 
				
			||||||
				avgTags: Math.round(db.prepare("SELECT avg(count) FROM (SELECT count(*) AS count FROM item_tag WHERE account = ? GROUP BY item_id)").pluck().get(account)*10)/10,
 | 
					 | 
				
			||||||
				lonelyTags: db.prepare("SELECT count(*) FROM (SELECT tag FROM item_tag WHERE account = ? GROUP BY tag HAVING count(*) = 1)").pluck().get(account),
 | 
					 | 
				
			||||||
				value: Math.round(select("item", ["currency", "price"], {account}).all().map(c => {
 | 
					 | 
				
			||||||
					return (currencyExchange.get(c.currency) || 0.6) * c.price / (currencyExchange.get(displayCurrency) || 1) / currencyRoundTo
 | 
					 | 
				
			||||||
				}).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: [...currencyExchange.keys()]
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (mode === "artist_grid") {
 | 
							if (mode === "artist_grid") {
 | 
				
			||||||
			loadPreviews(locals, "band_name", 4, whereClause, account, filter_field, filter, filter_fuzzy)
 | 
								loadPreviews(locals, "band_name", 4, whereClause, account, filter_field, filter, filter_fuzzy)
 | 
				
			||||||
| 
						 | 
					@ -186,7 +150,8 @@ router.get("/:account/", defineEventHandler({
 | 
				
			||||||
			locals.downloadManager = loadTags.downloadManager
 | 
								locals.downloadManager = loadTags.downloadManager
 | 
				
			||||||
			locals.downloader = loadTags.downloadManager.check(account)
 | 
								locals.downloader = loadTags.downloadManager.check(account)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		locals.hasFullTrackData = locals.count.tracks > locals.count.total
 | 
							// if there are any untagged items then we don't have full track data
 | 
				
			||||||
 | 
							locals.hasFullTrackData = !db.prepare("SELECT * FROM item LEFT JOIN item_tag USING (account, item_id) WHERE account = ? AND item_id = NULL").get(account)
 | 
				
			||||||
		return pugSync.render(event, `${arrange}_${shape}.pug`, locals)
 | 
							return pugSync.render(event, `${arrange}_${shape}.pug`, locals)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										63
									
								
								routes/collection-stats.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								routes/collection-stats.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {getCookie, defineEventHandler, readValidatedBody, setCookie} = require("h3")
 | 
				
			||||||
 | 
					const {z} = require("zod")
 | 
				
			||||||
 | 
					const {sync, select, db, router} = require("../passthrough")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @type {import("../pug-sync")} */
 | 
				
			||||||
 | 
					const pugSync = sync.require("../pug-sync")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const currencyExchange = new Map([
 | 
				
			||||||
 | 
						["AUD", 0.63],
 | 
				
			||||||
 | 
						["BRL", 0.17],
 | 
				
			||||||
 | 
						["CAD", 0.7],
 | 
				
			||||||
 | 
						["CHF", 1.13],
 | 
				
			||||||
 | 
						["EUR", 1.08],
 | 
				
			||||||
 | 
						["GBP", 1.3],
 | 
				
			||||||
 | 
						["JPY", 0.0067],
 | 
				
			||||||
 | 
						["NOK", 0.1],
 | 
				
			||||||
 | 
						["NZD", 0.57],
 | 
				
			||||||
 | 
						["USD", 1],
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					const currencies = [...currencyExchange.keys()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pugSync.beforeInclude("includes/collection-stats.pug", async (from, event, {account, currency}) => {
 | 
				
			||||||
 | 
						let displayCurrency = currency || getCookie(event, "bcex-currency") || ""
 | 
				
			||||||
 | 
						if (!currencyExchange.has(displayCurrency)) displayCurrency = "NZD"
 | 
				
			||||||
 | 
						const currencyRoundTo = displayCurrency === "JPY" ? 1000 : 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							count: {
 | 
				
			||||||
 | 
								total: db.prepare("SELECT count(*) FROM item WHERE account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								runtime: db.prepare("SELECT iif(sum(duration) > 86400, cast(total(duration)/86400 AS INTEGER) || 'd ' || cast(total(duration)/3600%24 AS INTEGER) || 'h', cast(total(duration)/3600 AS INTEGER) || 'h') FROM track WHERE account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								albums: db.prepare("SELECT count(*) FROM item WHERE item_type = 'album' AND account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								singles: db.prepare("SELECT count(*) FROM item WHERE item_type = 'track' AND account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								free: db.prepare("SELECT count(*) FROM item WHERE price = 0 AND account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								paid: db.prepare("SELECT count(*) FROM item WHERE price > 0 AND account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								tracks: db.prepare("SELECT count(*) FROM track WHERE account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								avgTracks: Math.round(db.prepare("SELECT avg(count) FROM (SELECT count(*) AS count FROM track INNER JOIN item USING (account, item_id) WHERE item_type = 'album' AND account = ? GROUP BY item_id)").pluck().get(account)*10)/10,
 | 
				
			||||||
 | 
								tags: db.prepare("SELECT count(*) FROM item_tag WHERE account = ?").pluck().get(account),
 | 
				
			||||||
 | 
								avgTags: Math.round(db.prepare("SELECT avg(count) FROM (SELECT count(*) AS count FROM item_tag WHERE account = ? GROUP BY item_id)").pluck().get(account)*10)/10,
 | 
				
			||||||
 | 
								lonelyTags: db.prepare("SELECT count(*) FROM (SELECT tag FROM item_tag WHERE account = ? GROUP BY tag HAVING count(*) = 1)").pluck().get(account),
 | 
				
			||||||
 | 
								value: Math.round(select("item", ["currency", "price"], {account}).all().map(c => {
 | 
				
			||||||
 | 
									return (currencyExchange.get(c.currency) || 0.6) * c.price / (currencyExchange.get(displayCurrency) || 1) / currencyRoundTo
 | 
				
			||||||
 | 
								}).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.post("/api/settings/currency", defineEventHandler(async event => {
 | 
				
			||||||
 | 
						const {currency, account} = await readValidatedBody(event, schema.currency.parse)
 | 
				
			||||||
 | 
						setCookie(event, "bcex-currency", currency)
 | 
				
			||||||
 | 
						return pugSync.render(event, "includes/collection-stats.pug", {account, currency})
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
| 
						 | 
					@ -2,15 +2,11 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {z} = require("zod")
 | 
					const {z} = require("zod")
 | 
				
			||||||
const {router} = require("../passthrough")
 | 
					const {router} = require("../passthrough")
 | 
				
			||||||
const {defineEventHandler, readValidatedBody, setCookie, setResponseHeader, sendRedirect} = require("h3")
 | 
					const {defineEventHandler, readValidatedBody, setCookie, setResponseHeader} = require("h3")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const schema = {
 | 
					const schema = {
 | 
				
			||||||
	inline_player: z.object({
 | 
						inline_player: z.object({
 | 
				
			||||||
		inline_player: z.string().optional()
 | 
							inline_player: z.string().optional()
 | 
				
			||||||
	}),
 | 
					 | 
				
			||||||
	currency: z.object({
 | 
					 | 
				
			||||||
		currency: z.string().regex(/^[A-Z]{3}$/),
 | 
					 | 
				
			||||||
		account: z.string()
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,9 +16,3 @@ router.post("/api/settings/inline-player", defineEventHandler(async event => {
 | 
				
			||||||
	setResponseHeader(event, "HX-Refresh", "true")
 | 
						setResponseHeader(event, "HX-Refresh", "true")
 | 
				
			||||||
	return null
 | 
						return null
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
 | 
					 | 
				
			||||||
router.post("/api/settings/currency", defineEventHandler(async event => {
 | 
					 | 
				
			||||||
	const {currency, account} = await readValidatedBody(event, schema.currency.parse)
 | 
					 | 
				
			||||||
	setCookie(event, "bcex-currency", currency)
 | 
					 | 
				
			||||||
	return sendRedirect(event, `/${account}/?arrange=tag&shape=grid`, 302)
 | 
					 | 
				
			||||||
}))
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								start.js
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								start.js
									
										
									
									
									
								
							| 
						 | 
					@ -38,6 +38,7 @@ sync.require("./routes/app")
 | 
				
			||||||
sync.require("./routes/load-collection")
 | 
					sync.require("./routes/load-collection")
 | 
				
			||||||
sync.require("./routes/play")
 | 
					sync.require("./routes/play")
 | 
				
			||||||
sync.require("./routes/settings")
 | 
					sync.require("./routes/settings")
 | 
				
			||||||
 | 
					sync.require("./routes/collection-stats")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Files
 | 
					// Files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue