* Bard Bivou(m)acs
** Notes
- =M-x html-html5-template= maybe?
- [[file:~/new-emacs/snippets/html-mode/audio::# -*- mode: snippet -*-][audio tag snippet]]
- [[file:~/new-emacs/straight/repos/emms/emms-info-opusinfo.el::(defun emms-info-opusinfo (track)][emms-info-opusinfo]]
*** full emms integration path
realizing a lot of the heavy lifting might be doable by
leveraging the emms playlist buffer / functionality.
#+name: track-list
#+begin_src emacs-lisp :var dir=""
(let ((buf (emms-playlist-new))
(buf2 (get-buffer-create "*foo*"))
(html))
(emms-playlist-set-playlist-buffer buf)
(with-current-buffer buf
(emms-playlist-clear)
(emms-add-directory dir)
(emms-walk-tracks
(let* ((track (emms-playlist-track-at (point)))
(file (file-name-nondirectory (cdr (assoc 'name (cdr track)))))
(title (cdr (assoc 'info-title (cdr track))))
(number (cdr (assoc 'info-tracknumber (cdr track)))))
(with-current-buffer buf2
(insert "
") (newline)
(insert (concat " " number ". " title)) (newline)
(insert (concat "\n \n "))
(goto-char (point-max))
(newline)
(insert " ")
(newline)))))
(with-current-buffer buf2
(html-mode)
(indent-region (point-min) (point-max))
(setq html (buffer-string)))
(kill-buffer "*foo*")
html)
#+end_src
#+RESULTS:
#+begin_example
1. akane
2. merculite missle
... and so on
#+end_example
** Structure
*** version 1
- [X] move html tags out of code as much as posible and put them here
- [ ] break up templates to fill them in easier with functions
- [X] add m3u or pls file link
- [ ] torrent?
This defines two "halves" dividing the page. I'm envisioning a horizontal
alignment on browser and vertical on mobile. It will display the cover art,
artist and album title, the tracklist, and whatever player controls I come
up with. This will be used as a format string in emacs-lisp code where we
will fill in the %s characters with the desired values.
#+name: structure
#+begin_export html
%s
#+end_export
I imagine with a package, you might have a custom variable that
could be used as a template file, or perhaps it could be a literal
template file given in the directory or as an argument. Or... I
have found [[https://github.com/Kinneyzhang/pp-html]] which is an elisp
library for generating HTML directly. However, in the effort to Keep
It Simple, Silly, plain HTML with %s's is fine for me.
Anyhow, this dynamic variable will allow the functions defined below
to use it assuming its assigned a proper format string form.
#+name: set-template
#+begin_src emacs-lisp :var templ=structure
(setq bard-bivoumacs-template templ)
#+end_src
#+RESULTS: set-template
#+begin_example
%s
#+end_example
*** version 2
#+name: page-title
#+begin_src html
casiopeia basement days
#+end_src
#+name: album-title
#+begin_src emacs-lisp
"Basement Days"
#+end_src
#+name: html-index
#+begin_src html :noweb yes :tangle index.html
<>
<>
#+end_src
#+name: head
#+begin_src html :noweb yes
<>
#+end_src
#+name: bard-bivoumacs
#+begin_src html :noweb yes
<>
#+end_src
#+name: a-half
#+begin_src html :noweb yes
#+end_src
#+name: b-half
#+begin_src html :noweb yes
#+end_src
#+name: footer
#+begin_src html
#+end_src
** Style
- [X] you can't style audio controls, so i need to build my own elements
for controlling with JS
- [X] pick a font or fonts
- [X] deal with view sizes
- [X] button images (swap play/pause etc)
*** Stylesheet
#+name: stylesheet
#+begin_src css :tangle style.css
@font-face {
font-family: 'UnifontMedium';
src: url('unifont.woff2');
font-weight: normal;
font-style: normal;
}
body {
background: lightblue;
font-family: 'UnifontMedium';
font-weight: normal;
font-style: normal;
display: flex;
flex-direction: column;
}
ul {
list-style: none;
padding-left: 0;
}
ul li::before {
content: "\200B";
}
#app {
display: flex;
align-items: center;
justify-content: space-around;
margin: auto;
width: 975px;
height: 800px;
}
#footer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
margin: auto;
width: 975px;
}
#license {
margin-top: 10px;
font-size: x-small;
display: flex;
align-items: center;
justify-content: space-evenly;
width: 50%;
}
.a-half {
height: 100%;
width: 50%;
display: flex;
flex-direction: column;
}
.controls {
height: 15%;
width: 100%;
display: flex;
justify-content: space-evenly;
}
.control {
border: 0;
background: transparent;
}
.track-list {
height: 85%;
}
.playlist-play {
height: 24px;
}
.b-half {
height: 100%;
width: 50%;
}
.album-title {
height: 20%;
}
.cover-art {
height: 80%;
}
.album-cover {
width: 100%;
}
@media (max-width: 500px) {
h1 {
font-size: medium;
}
#app {
display: flex;
align-items: center;
justify-content: space-around;
margin: auto;
flex-direction: column-reverse;
width: 100%;
height: 100%;
}
#footer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
margin: auto;
width: 100%;
}
#license {
margin-top: 10px;
font-size: x-small;
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
}
.a-half {
height: 50%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
}
.controls {
height: 10%;
}
.control-img {
height: 32px;
}
.track-list {
height: 90%;
}
.playlist-play {
height: 16px;
}
.b-half {
height: 50%;
width: 100%;
display: flex;
flex-direction: column-reverse;
align-items: center;
}
.album-title {
height: 15%;
}
.cover-art {
height: 85%;
}
.album-cover {
height: 100%;
width: 100%;
}
}
#+end_src
*** Images
#+name: play-svg
#+begin_src xml :tangle play-button.svg
#+end_src
[[file:play-button.svg]]
#+name: pause-svg
#+begin_src xml :tangle pause.svg
#+end_src
*** Fonts
#+name: russisch-sans
#+begin_export html
#+end_export
#+name: unifont
#+begin_src html
#+end_src
#+name: set-font
#+begin_src emacs-lisp :var font=unifont
(setq bard-bivoumacs-font font)
#+end_src
** Generator Code
The idea here is to read a directory that is prepared for release (see below)
and generate a nice web frontend to play and display an album.
- [X] read a directory for audio files
- [X] parse audio metadata
- [X] produce tag for each track
- [X] wrap it all up in a page including metadata
- [ ] support more audio formats
- [X] hook up to JS code for running the "playlist"
- [ ] generate playlist file (use emms and modify path to be relative)
#+begin_src emacs-lisp
(defun select-audio-files (dir audio-type)
"Returns a list of files from a directory DIR with given AUDIO-TYPE."
(seq-filter
(lambda (fn) (equal audio-type (file-name-extension fn)))
(directory-files dir)))
(defun make-track (file)
"Makes a track structure usable by EMMS info functions."
(list '*track* (cons 'type 'file) (cons 'name file)))
(defun process-dir-to-tracks (dir)
"Processes audio files from DIR into track structures usable with EMMS functions"
;; TODO fix opus / info function hardcode
(let ((tracks (mapcar #'make-track (select-audio-files "." "opus"))))
(mapcar #'emms-info-opusinfo tracks)
tracks))
(defun encode-track-list-element (track)
"Creates an HTML list element with TRACK"
(let ((file (cdr (assoc 'name (cdr track))))
(title (cdr (assoc 'info-title (cdr track))))
(number (cdr (assoc 'info-tracknumber (cdr track)))))
(insert "") (newline)
(insert (concat " " number ". " title)) (newline)
(insert (concat "\n \n "))
(goto-char (point-max))
(newline)
(insert " ")
(newline)))
(defun tracklist-html (tracks)
(with-temp-buffer
(html-mode)
(insert "") (newline)
(dolist (track tracks)
(encode-track-list-element track))
(insert " ")
(buffer-string)))
(defun generate-album-html (dir)
(interactive "DDirectory: ")
(let* ((tracks (process-dir-to-tracks dir))
(cover (concat (file-name-as-directory dir) "cover.png"))
(artist (cdr (assoc 'info-artist (cdr (cadr tracks)))))
(album (cdr (assoc 'info-album (cdr (cadr tracks)))))
(title (concat artist " - " album)))
(with-temp-file "basement-days.html"
(html-mode)
(insert
;; title(page) | tracklist | title | cover
(format bard-bivoumacs-template title (tracklist-html tracks) title cover))
(indent-region (point-min) (point-max)))))
#+end_src
#+RESULTS:
: generate-album-html
*** org block version
#+name: get-audio-files
#+begin_src emacs-lisp :var audio-type="opus" :var dir="~/Music/casiopeia/basement days" :exports none
(let ((default-directory dir))
(seq-filter
(lambda (fn) (equal audio-type (file-name-extension fn)))
(mapcar #'expand-file-name (directory-files dir))))
#+end_src
#+RESULTS: get-audio-files
| /home/shoshin/Music/casiopeia/basement days/1980 S-6.opus | /home/shoshin/Music/casiopeia/basement days/akane.opus | /home/shoshin/Music/casiopeia/basement days/bebop.opus | /home/shoshin/Music/casiopeia/basement days/bikini island.opus | /home/shoshin/Music/casiopeia/basement days/calypso drone i.opus | /home/shoshin/Music/casiopeia/basement days/calypso drone ii.opus | /home/shoshin/Music/casiopeia/basement days/calypso drone iii.opus | /home/shoshin/Music/casiopeia/basement days/canon.opus | /home/shoshin/Music/casiopeia/basement days/flirt.opus | /home/shoshin/Music/casiopeia/basement days/hardware.opus | /home/shoshin/Music/casiopeia/basement days/io fanfare.opus | /home/shoshin/Music/casiopeia/basement days/jacquelyne sofia kilmer.opus | /home/shoshin/Music/casiopeia/basement days/merculite missle.opus | /home/shoshin/Music/casiopeia/basement days/ripley.opus | /home/shoshin/Music/casiopeia/basement days/taxi medley.opus | /home/shoshin/Music/casiopeia/basement days/teseo.opus | /home/shoshin/Music/casiopeia/basement days/the slow train.opus |
#+name: convert-to-emms-tracks
#+begin_src emacs-lisp :noweb yes
(let ((tracks
(mapcar (lambda (file)
(list '*track* (cons 'type 'file) (cons 'name file)))
'<>)))
(mapcar #'emms-info-opusinfo tracks)
tracks)
#+end_src
#+RESULTS: convert-to-emms-tracks
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/1980 S-6.opus) | (info-playing-time . 199) | (info-playing-time-min . 3) | (info-playing-time-sec . 19) | (info-file . /home/shoshin/Music/casiopeia/basement days/1980 S-6.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . 1980 S-6) | (info-tracknumber . 4) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/akane.opus) | (info-playing-time . 413) | (info-playing-time-min . 6) | (info-playing-time-sec . 53) | (info-file . /home/shoshin/Music/casiopeia/basement days/akane.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . akane) | (info-tracknumber . 1) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/bebop.opus) | (info-playing-time . 97) | (info-playing-time-min . 1) | (info-playing-time-sec . 37) | (info-file . /home/shoshin/Music/casiopeia/basement days/bebop.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . bebop) | (info-tracknumber . 14) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/bikini island.opus) | (info-playing-time . 240) | (info-playing-time-min . 4) | (info-playing-time-sec . 0) | (info-file . /home/shoshin/Music/casiopeia/basement days/bikini island.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . bikini island) | (info-tracknumber . 12) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/calypso drone i.opus) | (info-playing-time . 52) | (info-playing-time-min . 0) | (info-playing-time-sec . 52) | (info-file . /home/shoshin/Music/casiopeia/basement days/calypso drone i.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . calypso drone i) | (info-tracknumber . 5) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/calypso drone ii.opus) | (info-playing-time . 194) | (info-playing-time-min . 3) | (info-playing-time-sec . 14) | (info-file . /home/shoshin/Music/casiopeia/basement days/calypso drone ii.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . calypso drone ii) | (info-tracknumber . 6) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/calypso drone iii.opus) | (info-playing-time . 72) | (info-playing-time-min . 1) | (info-playing-time-sec . 12) | (info-file . /home/shoshin/Music/casiopeia/basement days/calypso drone iii.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-title . calypso drone iii) | (info-tracknumber . 7) | (info-tracktotal . 17) | |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/canon.opus) | (info-playing-time . 441) | (info-playing-time-min . 7) | (info-playing-time-sec . 21) | (info-file . /home/shoshin/Music/casiopeia/basement days/canon.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . canon) | (info-tracknumber . 17) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/flirt.opus) | (info-playing-time . 233) | (info-playing-time-min . 3) | (info-playing-time-sec . 53) | (info-file . /home/shoshin/Music/casiopeia/basement days/flirt.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . flirt) | (info-tracknumber . 15) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/hardware.opus) | (info-playing-time . 90) | (info-playing-time-min . 1) | (info-playing-time-sec . 30) | (info-file . /home/shoshin/Music/casiopeia/basement days/hardware.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . hardware) | (info-tracknumber . 16) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/io fanfare.opus) | (info-playing-time . 129) | (info-playing-time-min . 2) | (info-playing-time-sec . 9) | (info-file . /home/shoshin/Music/casiopeia/basement days/io fanfare.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . io fanfare) | (info-tracknumber . 9) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/jacquelyne sofia kilmer.opus) | (info-playing-time . 466) | (info-playing-time-min . 7) | (info-playing-time-sec . 46) | (info-file . /home/shoshin/Music/casiopeia/basement days/jacquelyne sofia kilmer.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . jacquelyne sofia kilmer) | (info-tracknumber . 8) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/merculite missle.opus) | (info-playing-time . 280) | (info-playing-time-min . 4) | (info-playing-time-sec . 40) | (info-file . /home/shoshin/Music/casiopeia/basement days/merculite missle.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . merculite missle) | (info-tracknumber . 2) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/ripley.opus) | (info-playing-time . 183) | (info-playing-time-min . 3) | (info-playing-time-sec . 3) | (info-file . /home/shoshin/Music/casiopeia/basement days/ripley.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . ripley) | (info-tracknumber . 3) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/taxi medley.opus) | (info-playing-time . 327) | (info-playing-time-min . 5) | (info-playing-time-sec . 27) | (info-file . /home/shoshin/Music/casiopeia/basement days/taxi medley.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . taxi medley) | (info-tracknumber . 10) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/teseo.opus) | (info-playing-time . 198) | (info-playing-time-min . 3) | (info-playing-time-sec . 18) | (info-file . /home/shoshin/Music/casiopeia/basement days/teseo.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . teseo) | (info-tracknumber . 13) | (info-tracktotal . 17) |
| *track* | (type . file) | (name . /home/shoshin/Music/casiopeia/basement days/the slow train.opus) | (info-playing-time . 299) | (info-playing-time-min . 4) | (info-playing-time-sec . 59) | (info-file . /home/shoshin/Music/casiopeia/basement days/the slow train.opus) | (info-album . basement days) | (info-artist . casiopeia) | (info-date . 2000) | (info-encoder . Lavc58.35.100 libopus) | (info-genre . new wave) | (info-title . the slow train) | (info-tracknumber . 11) | (info-tracktotal . 17) |
#+name: track-to-html-list-item
#+begin_src emacs-lisp :var tracks=convert-to-emms-tracks()
(let ((file (cdr (assoc 'name (cdr (car tracks)))))
(title (cdr (assoc 'info-title (cdr (car tracks)))))
(number (cdr (assoc 'info-tracknumber (cdr (car tracks))))))
(with-temp-buffer
(insert "") (newline)
(insert (concat " " number ". " title)) (newline)
(insert (concat "\n \n "))
(goto-char (point-max))
(newline)
(insert " ")
(newline)
(buffer-string)))
#+end_src
#+RESULTS: track-to-html-list-item
:
: 4. 1980 S-6
:
:
:
:
** Playlist JS
- [X] register each audio element in a JS array
- [X] keep track of the current track
- [X] respond to track end event by starting next track
- [X] "main" controls for player
- [X] individual play buttons for each track to start from there
- [ ] ? shuffle ?
- [X] set current track time to 0 when seeking fd/bk
- [ ] Display time passed / total time of current track
#+name: playlist-js
#+begin_src javascript :tangle player.js
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0
var tracks = document.querySelectorAll("audio");
var trackIndex = 0
var currentTrack = tracks[trackIndex];
var mainPlay = document.getElementById("main-play");
var rewind = document.getElementById("rewind");
var forward = document.getElementById("forward");
var isPlaying = false;
var image = { play: "play.svg", pause: "pause.svg" }
function play() {
currentTrack.play();
mainPlay.firstChild.src = image.pause;
currentButton().src = image.pause;
isPlaying = true;
}
function pause() {
currentTrack.pause();
mainPlay.firstChild.src = image.play;
currentButton().src = image.play;
isPlaying = false;
}
function currentButton() {
return document.getElementById(`track-${trackIndex + 1}`).firstChild;
}
mainPlay.onclick = () => {
isPlaying ? pause() : play();
}
rewind.onclick = () => {
currentTrack.currentTime = 0;
if (trackIndex === 0) {
return;
} else {
pause();
trackIndex = trackIndex - 1;
currentTrack = tracks[trackIndex];
play();
}
}
forward.onclick = () => {
pause();
currentTrack.currentTime = 0;
if (trackIndex === tracks.length) {
trackIndex = 0;
currentTrack = tracks[0];
play();
} else {
trackIndex = trackIndex + 1;
currentTrack = tracks[trackIndex];
play();
}
}
tracks.forEach((track, index) => {
btn = document.getElementById(`track-${index + 1}`)
btn.onclick = () => {
pause();
trackIndex = index;
currentTrack = tracks[trackIndex];
play();
}
track.addEventListener("ended", () => {
currentButton().src = image.play;
trackIndex = trackIndex + 1;
currentTrack = tracks[trackIndex];
play();
})
});
// @license-end
#+end_src
** Content License
I've decided to use the [[https://creativecommons.org/licenses/by-sa/4.0/][CC BY-SA 4.0 license]]
which is the Creative Commons Attribution-ShareAlike that is similar
to the AGPL. The content can be used for any purpose, as long as
attribution is given to the author, and derivative works use a
compatible license.
#+name: license-html
#+begin_src html
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License .
#+end_src