aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md11
-rwxr-xr-xbin/build7
-rwxr-xr-xbin/dev-server17
-rw-r--r--flake.lock60
-rw-r--r--flake.nix21
-rw-r--r--main.py147
-rw-r--r--public/index.html15
-rw-r--r--public/main.css85
-rw-r--r--songs/ben-e-king/stand-by-me.lisp53
-rw-r--r--songs/graeme-allwright/petit-garcon.lisp45
-rw-r--r--src/main.lisp150
12 files changed, 612 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e54d92
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+public/songs
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b7a9b4c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Music
+
+Available at [https://music.guyonvarch.me](https://music.guyonvarch.me).
+
+## Getting started
+
+ nix develop --command bin/dev-server
+
+## Build
+
+ nix develop --command bin/build
diff --git a/bin/build b/bin/build
new file mode 100755
index 0000000..3230144
--- /dev/null
+++ b/bin/build
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+echo "Building…"
+sbcl --script src/main.lisp
+echo "Done."
diff --git a/bin/dev-server b/bin/dev-server
new file mode 100755
index 0000000..82f8d46
--- /dev/null
+++ b/bin/dev-server
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Serve public
+
+fuser -k 8000/tcp || true
+python -m http.server --directory public 8000 &
+trap "fuser -k 8000/tcp" EXIT
+
+# Build HTML recipes on changes
+
+watchexec \
+ --clear \
+ --restart \
+ --watch src \
+ --watch songs \
+ "bin/build"
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..16203c8
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,60 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1701680307,
+ "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1703685222,
+ "narHash": "sha256-zo8ud0+ldS8vYwl0PhNX214h0VhQDe0oKJgTtcy9Zps=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "5ff137aa81ff67b11459da96ea245970d9adc061",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..4b24639
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,21 @@
+{
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem
+ (system:
+ let pkgs = nixpkgs.legacyPackages.${system};
+ in {
+ devShell = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ psmisc # fuser
+ sbcl
+ watchexec
+ ];
+ };
+ }
+ );
+}
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..a4494ff
--- /dev/null
+++ b/main.py
@@ -0,0 +1,147 @@
+import string
+
+atom_end = set('()"\'') | set(string.whitespace)
+
+def parse(sexp):
+ stack, i, length = [[]], 0, len(sexp)
+ while i < length:
+ c = sexp[i]
+
+ reading = type(stack[-1])
+ if reading == list:
+ match c:
+ case '(':
+ stack.append([])
+ case ')':
+ stack[-2].append(stack.pop())
+ if stack[-1][0] == ('quote',): stack[-2].append(stack.pop())
+ case '"':
+ stack.append('')
+ case "'":
+ stack.append([('quote',)])
+ case _:
+ if c in string.whitespace:
+ pass
+ else:
+ stack.append((c,))
+ elif reading == str:
+ if c == '"':
+ stack[-2].append(stack.pop())
+ if stack[-1][0] == ('quote',): stack[-2].append(stack.pop())
+ elif c == '\\':
+ i += 1
+ stack[-1] += sexp[i]
+ else:
+ stack[-1] += c
+ elif reading == tuple:
+ if c in atom_end:
+ atom = stack.pop()
+ if atom[0][0].isdigit():
+ stack[-1].append(eval(atom[0]))
+ else:
+ stack[-1].append(atom)
+ if stack[-1][0] == ('quote',):
+ stack[-2].append(stack.pop())
+ continue
+ else:
+ stack[-1] = ((stack[-1][0] + c),)
+ i += 1
+ return stack.pop()
+
+def h(node, attributes, *children):
+ res_attrs = ''
+ if type(attributes) == dict:
+ for key in attributes:
+ res_attrs += f' {key}={attributes[key]}'
+ else:
+ children = [attributes] + list(children)
+
+ res = f'<{node} {res_attrs}>'
+ if type(children) in [tuple, list]:
+ res += ''.join(children)
+ elif type(children) == str:
+ res += children
+
+ res += f'</{node}>'
+ return res
+
+def lines(s: str):
+ return [h('div', x) for x in s.split('\n')]
+
+def chord_line(sexp):
+ res = ''
+ for x in sexp:
+ res += h('td', { 'class': 'g-Chord' }, x[0])
+ return res
+
+def chords(sexp):
+ return h('section',
+ { 'class': 'g-Section' },
+ h('h2', { 'class': 'g-Subtitle' }, 'Accords'),
+ h('table', { 'class': 'g-Chords' }, *[h('tr', {}, chord_line(x)) for x in sexp]))
+
+part_names = {
+ 'intro': 'Intro',
+ 'verse': 'Couplet',
+ 'chorus': 'Refrain',
+ 'interlude': 'Interlude'
+}
+
+def lyrics(sexp):
+ ys = []
+ for x in sexp:
+ match x:
+ case [(name)]:
+ ys.append(h('section',
+ { 'class': 'g-Section' },
+ h('div',
+ { 'class': 'g-Lyrics__Part' },
+ h('h3', part_names[name[0]]))))
+ case [(name), s]:
+ ys.append(h('section',
+ { 'class': 'g-Section' },
+ h('div',
+ { 'class': 'g-Lyrics__Part' },
+ h('h3', part_names[name[0]]),
+ h('div', { 'class': 'g-Lyrics__Paragraph' }, *lines(s)))))
+ return h('section',
+ { 'class': 'g-Section' },
+ h('h2', { 'class': 'g-Subtitle' }, 'Paroles'),
+ h('div', { 'class': 'g-Lyrics' }, *ys))
+
+def sexp_prop(sexp, name):
+ return [x[1:] for x in sexp if x[0][0] == name][0]
+
+# -----------------
+
+with open('ben-e-king-stand-by-me.lisp', 'r') as f:
+ content = f.read()
+
+sexp = parse(content)
+
+title = sexp_prop(sexp, 'title')[0]
+subtitle = sexp_prop(sexp, 'from')[0]
+
+html = f'''
+ <!doctype html>
+ <html lang="fr">
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>{title} – {subtitle}</title>
+ <link rel="stylesheet" href="main.css">
+ <link rel="icon" href="/icon.png">
+ <script src="/main.js"></script>
+'''
+
+html += h('body',
+ h('section',
+ { 'class': 'g-Section' },
+ h('h1', { 'class': 'g-Title' }, title),
+ h('div', { 'class': 'g-Author' }, subtitle)),
+ chords(sexp_prop(sexp, 'chords')),
+ lyrics(sexp_prop(sexp, 'lyrics')))
+
+with open('ben-e-king-stand-by-me.html', 'w') as f:
+ f.write(html)
+
+print('OK')
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..1aba3bd
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html lang="fr">
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width">
+<title>Music</title>
+<link rel="stylesheet" href="/main.css">
+<link rel="icon" href="/icon.png">
+
+<h1 class="g-Title">Music</h1>
+
+<ul class="g-Songs">
+ <li>
+ <a href="songs/ben-e-king/stand-by-me.html">Ben E. King – Stand by Me</a>
+ <li>
+ <a href="songs/graeme-allwright/petit-garcon.html">Graeme Allwright – Petit Garçon</a>
diff --git a/public/main.css b/public/main.css
new file mode 100644
index 0000000..5f6d450
--- /dev/null
+++ b/public/main.css
@@ -0,0 +1,85 @@
+body {
+ width: fit-content;
+ font-family: sans-serif;
+ margin: 2rem auto;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+/* Index */
+
+.g-Songs {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin: 0;
+}
+
+/* Song */
+
+.g-Back {
+ margin-bottom: 0;
+}
+
+.g-Title {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ font-size: 150%;
+}
+
+.g-Subtitle {
+ margin-top: 0;
+ margin-bottom: 1.5rem;
+ font-size: 130%;
+}
+
+.g-Author {
+ font-style: italic;
+}
+
+.g-Parts {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.g-Part {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.g-Part h3 {
+ margin: 0;
+ background-color: #f0f0f0;
+ padding: 0 0.25rem;
+ border-radius: 0.25rem;
+ font-size: 110%;
+ font-weight: normal;
+ font-family: monospace;
+ color: #851616;
+ width: fit-content;
+}
+
+.g-Chords {
+ border-collapse: collapse;
+}
+
+.g-Chords td {
+ width: 4rem;
+ text-align: center;
+ border-left: 1px solid black;
+ border-right: 1px solid black;
+ padding: 0.5rem 0;
+}
+
+.g-Lyrics__Paragraph {
+ display: flex;
+ flex-direction: column;
+ gap: 0.125rem;
+}
+
+.g-Lyrics__Paragraph emph {
+ background-color: #e6e6d2;
+}
diff --git a/songs/ben-e-king/stand-by-me.lisp b/songs/ben-e-king/stand-by-me.lisp
new file mode 100644
index 0000000..8ae7f2c
--- /dev/null
+++ b/songs/ben-e-king/stand-by-me.lisp
@@ -0,0 +1,53 @@
+(song
+ (title "Stand by Me")
+ (from "Ben E. King")
+
+ (chords
+ (all
+ (A % "F♯m" %)
+ (D E A %)))
+
+ (lyrics
+ (intro)
+
+ (verse
+ "When the night, has come,
+ And the land is dark,
+ And the moon, is the only, light we’ll see.
+ No I won’t, be afraid,
+ Oh I won’t, be afraid,
+ Just as long, as you stand, stand by me.")
+
+ (chorus
+ "So darlin’ darlin’ stand, by me,
+ Oh stand, by me,
+ Oh stand,
+ Stand by me, stand by me.")
+
+ (verse
+ "If the sky, that we look upon,
+ Should tumble and fall.
+ Or the mountains, should crumble, to the sea.
+ I won’t cry, I won’t cry,
+ No I won’t, shed a tear,
+ Just as long, as you stand, stand by me.")
+
+ (chorus
+ "And darlin’ darlin’ stand, by me,
+ Oh stand, by me,
+ Woah stand now,
+ Stand by me, stand by me.")
+
+ (interlude)
+
+ (chorus
+ "Darlin’ darlin’ stand, by me,
+ Oh stand, by me,
+ Oh stand now,
+ Stand by me, stand by me.")
+
+ (verse
+ "Whenever you’re in trouble won’t you stand, by me?
+ Oh stand, by me
+ Woah just stand now,
+ Oh stand, stand by me.")))
diff --git a/songs/graeme-allwright/petit-garcon.lisp b/songs/graeme-allwright/petit-garcon.lisp
new file mode 100644
index 0000000..1476eb9
--- /dev/null
+++ b/songs/graeme-allwright/petit-garcon.lisp
@@ -0,0 +1,45 @@
+(song
+ (title "Petit Garçon")
+ (from "Graeme Allwright")
+
+ (chords
+ (verse
+ (G D C D7)
+ ("G / G7" "C / C-" "G / D7" G))
+
+ (chorus
+ (D7 % C G)
+ (Em "Em / A7" "A7 / D7" D7)))
+
+ (lyrics
+ (intro)
+
+ (verse
+ "Dans son manteau, rouge et blanc,
+ Sur un traîneau, porté par le vent,
+ Il descendra, par la cheminée,
+ Petit garçon il est l’heure d’aller se coucher.")
+
+ (chorus
+ "Tes yeux se voilent,
+ Écoute les étoiles.
+ Tout est calme, reposé,
+ Entends-tu les clochettes tintinnabuler ?")
+
+ (verse
+ "Et demain matin, petit garçon,
+ Tu trouveras, dans tes chaussons,
+ Tous les jouets, dont tu as rêvé,
+ Petit garçon il est l’heure d’aller se coucher.")
+
+ (chorus
+ "Tes yeux se voilent,
+ Écoute les étoiles,
+ Tout est calme, reposé,
+ Entends-tu les clochettes tintinnabuler ?")
+
+ (verse
+ "Et demain matin, petit garçon,
+ Tu trouveras dans tes chaussons,
+ Tous les jouets dont tu as rêvé,
+ Petit garçon il est l’heure d’aller se coucher.")))
diff --git a/src/main.lisp b/src/main.lisp
new file mode 100644
index 0000000..8e47016
--- /dev/null
+++ b/src/main.lisp
@@ -0,0 +1,150 @@
+; Helpers
+
+(defun lines (str)
+ (split str #\linefeed))
+
+(defun split (str c)
+ (loop
+ for i = 0 then (1+ j)
+ as j = (position c str :start i)
+ collect (subseq str i j)
+ while j))
+
+(defun write-file (filename s)
+ (ensure-directories-exist filename)
+ (with-open-file (str filename
+ :direction :output
+ :if-exists :supersede
+ :if-does-not-exist :create)
+ (format str "~A" s)))
+
+; Data
+
+(defun song-key (key song)
+ (let ((tuple (car song)))
+ (cond
+ ((eql tuple nil) nil)
+ ((eql (car tuple) key) (cdr tuple))
+ (t (song-key key (cdr song))))))
+
+; HTML
+
+(defun page (title body)
+ (format
+ nil
+ "<!doctype html><html lang=\"fr\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>~A</title><link rel=\"stylesheet\" href=\"/main.css\"><link rel=\"icon\" href=\"/icon.png\">~A"
+ title
+ body))
+
+(defun h (node attrs children)
+ (format
+ nil
+ "<~A ~A>~A</~A>"
+ node
+ (apply #'concatenate 'string (mapcar (lambda (x) (format nil " ~A=\"~A\"" (car x) (car (cdr x)))) attrs))
+ (apply #'concatenate 'string children)
+ node))
+
+; Title
+
+(defun title-tags (title from)
+ (h "section" nil
+ (list
+ (h "h1" '(("class" "g-Title")) (list title))
+ (h "div" '(("class" "g-Author")) (list from)))))
+
+; Part
+
+(defun part-name (key)
+ (ecase key
+ ('intro "Intro")
+ ('verse "Couplet")
+ ('chorus "Refrain")
+ ('interlude "Interlude")))
+
+(defun part-tags (key children)
+ (h "div"
+ '(("class" "g-Part"))
+ (list
+ (if
+ (eql key 'all)
+ nil
+ (h "h3" nil (list (part-name key))))
+ children)))
+
+; Chords
+
+(defun chord-row (chords)
+ (h "tr" nil (mapcar (lambda (x) (h "td" nil (list (string x)))) chords)))
+
+(defun chord-rows (xs)
+ (if
+ (eql xs nil)
+ nil
+ (cons (chord-row (car xs)) (chord-rows (cdr xs)))))
+
+(defun chord-table (key row)
+ (part-tags
+ key
+ (h "table" '(("class" "g-Chords")) (chord-rows row))))
+
+(defun chord-tables (xs)
+ (if
+ (eql xs nil)
+ nil
+ (let ((key (car (car xs)))
+ (rows (cdr (car xs))))
+ (cons (chord-table key rows) (chord-tables (cdr xs))))))
+
+(defun chord-tags (chords)
+ (h "section" nil
+ (list
+ (h "h2" '(("class" "g-Subtitle")) '("Accords"))
+ (h "div" '(("class" "g-Parts")) (chord-tables chords)))))
+
+; Lyrics
+
+(defun emph (str cs)
+ (apply #'concatenate 'string
+ (loop for c across str collect
+ (if (member c cs) (h "emph" nil (list (make-string 1 :initial-element c))) (make-string 1 :initial-element c)))))
+
+(defun lyrics-line (line)
+ (h "div" nil (list (emph line (list #\, #\. #\? #\!)))))
+
+(defun lyrics-section (s)
+ (let ((p (car (cdr s))))
+ (part-tags
+ (car s)
+ (if p
+ (h "div" '(("class" "g-Lyrics__Paragraph")) (mapcar #'lyrics-line (lines p)))
+ nil))))
+
+(defun lyrics-tags (lyrics)
+ (h "section" nil
+ (list
+ (h "h2" '(("class" "g-Subtitle")) '("Paroles"))
+ (h "div" '(("class" "g-Parts")) (mapcar #'lyrics-section lyrics)))))
+
+; Main
+
+(defun export-song (path)
+ (let ((data (with-open-file (in path) (read in))))
+ (let ((output (concatenate 'string "public/" (car (split path #\.)) ".html"))
+ (title (car (song-key 'title (cdr data))))
+ (from (car (song-key 'from (cdr data))))
+ (chords (song-key 'chords (cdr data)))
+ (lyrics (song-key 'lyrics (cdr data))))
+ (write-file output (page
+ (format nil "~A – ~A" title from)
+ (h
+ "body"
+ nil
+ (list
+ (h "a" '(("class" "g-Back") ("href" "/")) '("Retour à l’accueil"))
+ (title-tags title from)
+ (chord-tags chords)
+ (lyrics-tags lyrics))))))))
+
+(export-song "songs/graeme-allwright/petit-garcon.lisp")
+(export-song "songs/ben-e-king/stand-by-me.lisp")