Add embedded player
This commit is contained in:
		
							parent
							
								
									70ce8ab72b
								
							
						
					
					
						commit
						aa1095eef2
					
				
					 11 changed files with 160 additions and 110 deletions
				
			
		
							
								
								
									
										10
									
								
								public/player-marker.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								public/player-marker.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					function movePlayer() {
 | 
				
			||||||
 | 
						const pc = document.getElementById("player-container")
 | 
				
			||||||
 | 
						const playerExists = pc.querySelector("iframe")
 | 
				
			||||||
 | 
						if (!playerExists) return
 | 
				
			||||||
 | 
						const pm = document.getElementById("player-marker")
 | 
				
			||||||
 | 
						pm.style.height = `${pc.clientHeight}px`
 | 
				
			||||||
 | 
						pc.style.top = `${Math.round(pm.getBoundingClientRect().top)}px`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					movePlayer()
 | 
				
			||||||
 | 
					document.body.addEventListener("htmx:afterSettle", movePlayer)
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ block view
 | 
				
			||||||
    .d-grid.gx8.gy12.jc-center.break-word(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
 | 
					      each item in items
 | 
				
			||||||
        div
 | 
					        div
 | 
				
			||||||
          a.album-grid-link(href=item.item_url target="_blank")
 | 
					          a.s-link.album-grid-link(href=`/api/play/${item.item_type}/${item.item_id}` hx-target="#player" hx-select="#player" hx-indicator="null" hx-push-url="false")
 | 
				
			||||||
            img(loading="lazy" src=item.item_art_url width=210 height=210)
 | 
					            img(loading="lazy" src=item.item_art_url width=210 height=210)
 | 
				
			||||||
            p.fs-body3.mb8= item.item_title
 | 
					            p.fs-body3.mb8= item.item_title
 | 
				
			||||||
          .d-flex.fw-wrap.g4
 | 
					          .d-flex.fw-wrap.g4
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ html
 | 
				
			||||||
    meta(charset="utf-8")
 | 
					    meta(charset="utf-8")
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
				
			||||||
    title BC Explorer
 | 
					    title BC Explorer
 | 
				
			||||||
 | 
					    link(rel="icon" href="/favicon.png")
 | 
				
			||||||
    link(rel="stylesheet" type="text/css" href="/static/stacks.min.css")
 | 
					    link(rel="stylesheet" type="text/css" href="/static/stacks.min.css")
 | 
				
			||||||
    script(src="/static/htmx.js")
 | 
					    script(src="/static/htmx.js")
 | 
				
			||||||
    meta(name="htmx-config" content='{"requestClass":"is-loading"}')
 | 
					    meta(name="htmx-config" content='{"requestClass":"is-loading"}')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,7 @@ html
 | 
				
			||||||
    link(rel="stylesheet" type="text/css" href="/static/stacks.min.css")
 | 
					    link(rel="stylesheet" type="text/css" href="/static/stacks.min.css")
 | 
				
			||||||
    script(src="/static/htmx.js")
 | 
					    script(src="/static/htmx.js")
 | 
				
			||||||
    script(src="/static/wordcloud.js")
 | 
					    script(src="/static/wordcloud.js")
 | 
				
			||||||
 | 
					    script(defer src="/static/player-marker.js")
 | 
				
			||||||
    meta(name="htmx-config" content='{"requestClass":"is-loading"}')
 | 
					    meta(name="htmx-config" content='{"requestClass":"is-loading"}')
 | 
				
			||||||
    style.
 | 
					    style.
 | 
				
			||||||
      .themed {
 | 
					      .themed {
 | 
				
			||||||
| 
						 | 
					@ -69,7 +70,8 @@ html
 | 
				
			||||||
      svg {
 | 
					      svg {
 | 
				
			||||||
        flex-shrink: 0;
 | 
					        flex-shrink: 0;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
  body.themed.theme-system.overflow-y-scroll(hx-boost="true")
 | 
					  body.themed.theme-system.overflow-y-scroll(hx-boost="true" hx-swap="outerHTML" hx-target="#page" hx-select="#page")
 | 
				
			||||||
 | 
					    #page
 | 
				
			||||||
      header.s-topbar.ps-sticky.t0
 | 
					      header.s-topbar.ps-sticky.t0
 | 
				
			||||||
        .s-topbar--container.wmx9
 | 
					        .s-topbar--container.wmx9
 | 
				
			||||||
          .s-topbar--logo
 | 
					          .s-topbar--logo
 | 
				
			||||||
| 
						 | 
					@ -90,15 +92,13 @@ html
 | 
				
			||||||
              li: +navi("shape", "grid").brr0!= icons.get("view-grid")
 | 
					              li: +navi("shape", "grid").brr0!= icons.get("view-grid")
 | 
				
			||||||
              li: +navi("shape", "list").blr0!= icons.get("align-justify")
 | 
					              li: +navi("shape", "list").blr0!= icons.get("align-justify")
 | 
				
			||||||
          .fl-grow1
 | 
					          .fl-grow1
 | 
				
			||||||
        #player(hx-preserve)
 | 
					 | 
				
			||||||
          button.s-btn.s-btn__outlined.s-btn__xs!= icons.get("play")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .d-flex.py24.px16.g24.fs-body1
 | 
					      .d-flex.py24.px16.g24.fs-body1
 | 
				
			||||||
      .fl-grow1
 | 
					        main.fl-grow1
 | 
				
			||||||
          block view
 | 
					          block view
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      div
 | 
					        aside.ws3
 | 
				
			||||||
        .ps-sticky.d-flex.fd-column.g12.wmx4(style="top: 80px")
 | 
					          .ps-sticky.d-flex.fd-column.g12(style="top: 80px")
 | 
				
			||||||
            if arrange === "tag"
 | 
					            if arrange === "tag"
 | 
				
			||||||
              include tag-status.pug
 | 
					              include tag-status.pug
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,7 +120,9 @@ html
 | 
				
			||||||
                input(type="hidden" name="shape" value=shape)
 | 
					                input(type="hidden" name="shape" value=shape)
 | 
				
			||||||
                button.s-btn.s-btn__xs.s-btn__icon.s-btn__outlined.s-btn__muted#search-submit(style="height: 38px")!= icons.get("search")
 | 
					                button.s-btn.s-btn__xs.s-btn__icon.s-btn__outlined.s-btn__muted#search-submit(style="height: 38px")!= icons.get("search")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          .s-sidebarwidget.wmn3
 | 
					            #player-marker.pe-none.myn6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .s-sidebarwidget
 | 
				
			||||||
              .s-sidebarwidget--header Collection
 | 
					              .s-sidebarwidget--header Collection
 | 
				
			||||||
              table.s-sidebarwidget--content.s-sidebarwidget__items
 | 
					              table.s-sidebarwidget--content.s-sidebarwidget__items
 | 
				
			||||||
                tr.s-sidebarwidget--item
 | 
					                tr.s-sidebarwidget--item
 | 
				
			||||||
| 
						 | 
					@ -174,3 +176,6 @@ html
 | 
				
			||||||
                      .s-progress--bar.bg-yellow-400(title=`${count.taste[1]} labels with 20-199 fans`)= count.taste[1]
 | 
					                      .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-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]
 | 
					                      .s-progress--bar.bg-red-400(title=`${count.taste[3]} labels with >2000 fans`)= count.taste[3]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #player-container.ps-fixed.r16.ws3(hx-preserve="true")
 | 
				
			||||||
 | 
					      #player
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,14 @@
 | 
				
			||||||
if downloader.total > 0 || downloader.outcome
 | 
					if downloader.total > 0 || downloader.outcome
 | 
				
			||||||
  #tag-download
 | 
					  #tag-download
 | 
				
			||||||
    if !downloader.running && !downloader.outcome
 | 
					    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")
 | 
					      form.s-notice.s-notice__info.d-flex.ai-center.p8.gx16.pl16(role="status" hx-target="#tag-download" hx-post="/api/tag-download")
 | 
				
			||||||
        != icons.get("info-circle")
 | 
					        != icons.get("info-circle")
 | 
				
			||||||
        div Tag data needs to be downloaded. This will take a while.
 | 
					        div Tag data needs to be downloaded. This will take a while.
 | 
				
			||||||
        input(type="hidden" name="account" value=account)
 | 
					        input(type="hidden" name="account" value=account)
 | 
				
			||||||
        button.s-btn.s-btn__outlined Download now
 | 
					        button.s-btn.s-btn__outlined Download now
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else if !downloader.outcome
 | 
					    else if !downloader.outcome
 | 
				
			||||||
      .s-notice.p16(role="status" hx-swap="outerHTML" hx-target="#tag-download" hx-get=`/api/tag-download?account=${account}` hx-trigger="every 5s" hx-indicator="null")
 | 
					      .s-notice.p16(role="status" hx-target="#tag-download" hx-get=`/api/tag-download?account=${account}` hx-trigger="every 5s" hx-indicator="null")
 | 
				
			||||||
        .d-flex.gx16.ai-center
 | 
					        .d-flex.gx16.ai-center
 | 
				
			||||||
          != icons.get("cloud-download")
 | 
					          != icons.get("cloud-download")
 | 
				
			||||||
          .fl-grow1 Downloading tags...
 | 
					          .fl-grow1 Downloading tags...
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ if downloader.total > 0 || downloader.outcome
 | 
				
			||||||
          span= downloader.total
 | 
					          span= downloader.total
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else if downloader.outcome === "Success"
 | 
					    else if downloader.outcome === "Success"
 | 
				
			||||||
      .s-notice.s-notice__success.p8.gx16.pl16.d-flex.ai-center.wmn3
 | 
					      .s-notice.s-notice__success.p8.gx16.pl16.d-flex.ai-center
 | 
				
			||||||
        title#title(hx-swap-oob="true") * Tags downloaded! | BC Explorer
 | 
					        title#title(hx-swap-oob="true") * Tags downloaded! | BC Explorer
 | 
				
			||||||
        != icons.get("cloud-check")
 | 
					        != icons.get("cloud-check")
 | 
				
			||||||
        .fl-grow1 Tags downloaded.
 | 
					        .fl-grow1 Tags downloaded.
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ if downloader.total > 0 || downloader.outcome
 | 
				
			||||||
        a.s-btn.s-btn__outlined(href="") Refresh
 | 
					        a.s-btn.s-btn__outlined(href="") Refresh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      .s-notice.s-notice__danger.p8.gx16.pl16.d-flex.ai-center.wmn3
 | 
					      .s-notice.s-notice__danger.p8.gx16.pl16.d-flex.ai-center
 | 
				
			||||||
        title#title(hx-swap-oob="true") * Tag download failed! | BC Explorer
 | 
					        title#title(hx-swap-oob="true") * Tag download failed! | BC Explorer
 | 
				
			||||||
        != icons.get("cloud-xmark")
 | 
					        != icons.get("cloud-xmark")
 | 
				
			||||||
        .fl-grow1= downloader.outcome
 | 
					        .fl-grow1= downloader.outcome
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								pug/player.pug
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								pug/player.pug
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					#player
 | 
				
			||||||
 | 
					  .s-sidebarwidget(style="overflow: hidden")
 | 
				
			||||||
 | 
					    div(style="margin: -1px; margin-bottom: -11px").ps-relative
 | 
				
			||||||
 | 
					      a.ps-absolute.bg-white.bar0.t0.r0.s-btn.s-btn__icon.s-btn__muted.s-btn__sm.px16(href=`/api/play/${item_type}/${item_id}` hx-target="#player" hx-select="#player" hx-push-url="false").fc-theme-primary!= icons.get("refresh-double")
 | 
				
			||||||
 | 
					      iframe(style="border: 0; width: 100%; height: 424px;" src=`https://bandcamp.com/EmbeddedPlayer/${item_type}=${item_id}/size=large/bgcol=ffffff/linkcol=63b2cc/artwork=none/transparent=true/`)
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ const schema = {
 | 
				
			||||||
	query: z.object({
 | 
						query: z.object({
 | 
				
			||||||
		arrange: z.enum(["album", "artist", "label", "tag", "track"]),
 | 
							arrange: z.enum(["album", "artist", "label", "tag", "track"]),
 | 
				
			||||||
		shape: z.enum(["grid", "list"]),
 | 
							shape: z.enum(["grid", "list"]),
 | 
				
			||||||
		filter_field: z.enum(["band_name", "band_url", "item_id", "tag", "why"]).optional(),
 | 
							filter_field: z.enum(["band_name", "band_url", "item_title", "item_id", "tag", "why"]).optional(),
 | 
				
			||||||
		filter: z.string().optional(),
 | 
							filter: z.string().optional(),
 | 
				
			||||||
		filter_fuzzy: z.enum(["true"]).optional()
 | 
							filter_fuzzy: z.enum(["true"]).optional()
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,7 +61,7 @@ async function loadCollection(inputUsername) {
 | 
				
			||||||
	const preparedTrack = 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)")
 | 
						const preparedTrack = 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)")
 | 
				
			||||||
	db.transaction(() => {
 | 
						db.transaction(() => {
 | 
				
			||||||
		for (const [key, tracklist] of Object.entries(items.tracklists)) {
 | 
							for (const [key, tracklist] of Object.entries(items.tracklists)) {
 | 
				
			||||||
			assert.match(key[0], /[at]/)
 | 
								if (!key[0].match(/[at]/)) continue
 | 
				
			||||||
			for (const track of tracklist) {
 | 
								for (const track of tracklist) {
 | 
				
			||||||
				preparedTrack.run({
 | 
									preparedTrack.run({
 | 
				
			||||||
					account,
 | 
										account,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										19
									
								
								routes/play.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								routes/play.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const {z} = require("zod")
 | 
				
			||||||
 | 
					const {sync, router} = require("../passthrough")
 | 
				
			||||||
 | 
					const {defineEventHandler} = require("h3")
 | 
				
			||||||
 | 
					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})
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.get("/api/play/:item_type/:item_id", defineEventHandler(async event => {
 | 
				
			||||||
 | 
						const locals = await getValidatedRouterParams(event, schema.parse)
 | 
				
			||||||
 | 
						return pugSync.render(event, "player.pug", locals)
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
| 
						 | 
					@ -58,7 +58,7 @@ const har = JSON.parse(fs.readFileSync("scripts/account.har", "utf8"))
 | 
				
			||||||
	const preparedTrack = db.prepare("INSERT OR IGNORE INTO track (account, item_id, track_id, title, artist, track_number, duration, mp3) VALUES (@account, @item_id, @track_id, @title, @artist, @track_number, @duration, @mp3)")
 | 
						const preparedTrack = db.prepare("INSERT OR IGNORE INTO track (account, item_id, track_id, title, artist, track_number, duration, mp3) VALUES (@account, @item_id, @track_id, @title, @artist, @track_number, @duration, @mp3)")
 | 
				
			||||||
	db.transaction(() => {
 | 
						db.transaction(() => {
 | 
				
			||||||
		for (const [key, tracklist] of Object.entries(items.tracklists)) {
 | 
							for (const [key, tracklist] of Object.entries(items.tracklists)) {
 | 
				
			||||||
			assert.match(key[0], /[at]/)
 | 
								if (!key[0].match(/[at]/)) continue
 | 
				
			||||||
			for (const track of tracklist) {
 | 
								for (const track of tracklist) {
 | 
				
			||||||
				preparedTrack.run({
 | 
									preparedTrack.run({
 | 
				
			||||||
					account,
 | 
										account,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								start.js
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								start.js
									
										
									
									
									
								
							| 
						 | 
					@ -36,6 +36,7 @@ pugSync.createRoute(router, "/", "home.pug")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sync.require("./routes/app")
 | 
					sync.require("./routes/app")
 | 
				
			||||||
sync.require("./routes/load-collection")
 | 
					sync.require("./routes/load-collection")
 | 
				
			||||||
 | 
					sync.require("./routes/play")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Files
 | 
					// Files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,7 +54,9 @@ router.get("/static/htmx.js", defineEventHandler({
 | 
				
			||||||
	handler: async event => {
 | 
						handler: async event => {
 | 
				
			||||||
		handleCacheHeaders(event, {maxAge: 86400})
 | 
							handleCacheHeaders(event, {maxAge: 86400})
 | 
				
			||||||
		defaultContentType(event, "text/javascript")
 | 
							defaultContentType(event, "text/javascript")
 | 
				
			||||||
		return fs.promises.readFile(require.resolve("htmx.org/dist/htmx.js"), "utf-8")
 | 
							return Promise.all(["htmx.org/dist/htmx.js"].map(r =>
 | 
				
			||||||
 | 
								fs.promises.readFile(require.resolve(r), "utf-8")
 | 
				
			||||||
 | 
							)).then(files => files.join("\n\n\n"))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,6 +69,13 @@ router.get("/static/wordcloud.js", defineEventHandler({
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}))
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.get("/static/player-marker.js", defineEventHandler({
 | 
				
			||||||
 | 
						handler: async event => {
 | 
				
			||||||
 | 
							defaultContentType(event, "text/javascript")
 | 
				
			||||||
 | 
							return fs.promises.readFile("public/player-marker.js")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get("/favicon.png", defineEventHandler({
 | 
					router.get("/favicon.png", defineEventHandler({
 | 
				
			||||||
	handler: async event => {
 | 
						handler: async event => {
 | 
				
			||||||
		handleCacheHeaders(event, {maxAge: 86400})
 | 
							handleCacheHeaders(event, {maxAge: 86400})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue