All files / Bandstand/src/lib chart-pdf.ts

50.64% Statements 39/77
100% Branches 8/8
40% Functions 2/5
50.64% Lines 39/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 1071x             1x                                   1x 5x 5x 5x 5x 5x 5x     1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x   1x                     1x                                                                
import {
  detectKey,
  parseChordPro,
  semitonesBetween,
  transposeChart,
} from './chordpro'
import type {PdfCue, PdfItem} from './pdf'
import {displayName} from './songs'
 
type SongLike = {
  title: string
  writer: string | null
  leadSinger: string | null
}
type VersionLike = {
  number: number
  source: string
  createdAt: Date
  author: {
    name: string | null
    email: string | null
    displayName?: string | null
  } | null
}
 
const dateFmt = (timeZone: string) =>
  new Intl.DateTimeFormat('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
    timeZone,
  })
 
/** Build one PDF page's content; `key` transposes to that key when given. */
export function buildPdfItem(
  band: {name: string; timezone: string},
  song: SongLike,
  version: VersionLike,
  opts: {key?: string | null; note?: string | null} = {},
): PdfItem {
  const original = parseChordPro(version.source)
  const fromKey = detectKey(original)
  const steps = opts.key && fromKey ? semitonesBetween(fromKey, opts.key) : 0
  const chart = transposeChart(original, steps)
  const key = detectKey(chart)
  const subtitle = [
    song.writer,
    song.leadSinger && `Lead: ${song.leadSinger}`,
    key && `Key: ${key}${steps && fromKey ? ` (original ${fromKey})` : ''}`,
  ]
    .filter(Boolean)
    .join('   ·   ')
  return {
    title: song.title,
    subtitle,
    chart,
    note: opts.note,
    originalKey: fromKey,
    steps,
    footer: `${band.name}  ·  ${song.title}  ·  version ${version.number}, edited by ${displayName(version.author)} on ${dateFmt(band.timezone).format(version.createdAt)}`,
  }
}
 
export function pdfResponse(buf: Buffer, filename: string, download: boolean) {
  const safe = filename.replace(/[^\w .()'-]+/g, '').trim() || 'chart'
  return new Response(new Uint8Array(buf), {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `${download ? 'attachment' : 'inline'}; filename="${safe}.pdf"`,
      'Cache-Control': 'private, no-store',
    },
  })
}
 
export function paperFrom(url: URL): 'LETTER' | 'A4' {
  return url.searchParams.get('paper')?.toUpperCase() === 'A4' ? 'A4' : 'LETTER'
}
 
/** Someone's own cues on these songs, with picture bytes, for a PDF. */
export async function pdfCues(userId: string, songIds: string[]) {
  const {prisma} = await import('./db')
  const rows = await prisma.cue.findMany({
    where: {userId, songId: {in: songIds}},
    include: {image: true},
  })
  const out = new Map<string, PdfCue[]>()
  for (const r of rows)
    out.set(r.songId, [
      ...(out.get(r.songId) ?? []),
      {
        id: r.id,
        anchor: r.anchor,
        position: r.position,
        kind: r.kind,
        text: r.text,
        image: r.image
          ? {
              data: Buffer.from(r.image.data),
              width: r.image.width,
              height: r.image.height,
            }
          : null,
      },
    ])
  return out
}