diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 11 | ||||
-rwxr-xr-x | bin/build | 7 | ||||
-rwxr-xr-x | bin/dev-server | 17 | ||||
-rw-r--r-- | flake.lock | 60 | ||||
-rw-r--r-- | flake.nix | 21 | ||||
-rw-r--r-- | main.py | 147 | ||||
-rw-r--r-- | public/index.html | 15 | ||||
-rw-r--r-- | public/main.css | 85 | ||||
-rw-r--r-- | songs/ben-e-king/stand-by-me.lisp | 53 | ||||
-rw-r--r-- | songs/graeme-allwright/petit-garcon.lisp | 45 | ||||
-rw-r--r-- | src/main.lisp | 150 |
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 + ]; + }; + } + ); +} @@ -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") |