From 56379a104f30494d42bf87e3ee67cdb7089ddbeb Mon Sep 17 00:00:00 2001
From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com>
Date: Mon, 6 May 2024 16:59:04 +0200
Subject: [PATCH] Adding support for audio files

---
 AUTHORS                  |   2 +-
 src/sigal/audio.py       |  41 +++++++++++++++++++++++++++++++++++++++
 src/sigal/audio_file.png | Bin 0 -> 1799 bytes
 src/sigal/gallery.py     |  32 ++++++++++++++++++++++++++++++
 src/sigal/settings.py    |   3 +++
 src/sigal/utils.py       |  11 ++++++++++-
 6 files changed, 87 insertions(+), 2 deletions(-)
 create mode 100644 src/sigal/audio.py
 create mode 100644 src/sigal/audio_file.png

diff --git a/AUTHORS b/AUTHORS
index 9be0cc40..49e818fd 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -36,7 +36,7 @@ alphabetical order):
 - Keith Feldman
 - Keith Johnson
 - Kevin Murray
-- Lucas Cimon
+- Lucas Cimon (@Lucas-C)
 - Lukas Vacek
 - Luke Cyca (@lukecyca)
 - Mate Lakat
diff --git a/src/sigal/audio.py b/src/sigal/audio.py
new file mode 100644
index 00000000..2b031c44
--- /dev/null
+++ b/src/sigal/audio.py
@@ -0,0 +1,41 @@
+# Copyright (c)      2013 - Christophe-Marie Duquesne
+# Copyright (c) 2013-2023 - Simon Conseil
+# Copyright (c)      2021 - Keith Feldman
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+from os.path import dirname, join
+
+from . import utils
+from .utils import is_valid_html5_audio
+
+AUDIO_THUMB_FILE = join(dirname(__file__), "audio_file.png")
+
+
+def process_audio(media):
+    """Process an audio file: create thumbnail."""
+    settings = media.settings
+
+    with utils.raise_if_debug() as status:
+        utils.copy(media.src_path, media.dst_path, symlink=settings["orig_link"])
+
+    if settings["make_thumbs"]:
+        utils.copy(AUDIO_THUMB_FILE, media.thumb_path)
+
+    return status.value
diff --git a/src/sigal/audio_file.png b/src/sigal/audio_file.png
new file mode 100644
index 0000000000000000000000000000000000000000..b30845e6fad9c064eafb7bc7f2176a60d45ad80b
GIT binary patch
literal 1799
zcmaJ?c~sJg7XD?Lg$qxQD^mI<@u(=Gq9HDm+e=7AQAn~yW$`&V=7O4Jxn-`Y<pSbD
zC1welVOgPN>Zl3i*0_}EWN86y+T_;LX=wg==lwD7obTT6+<VV=&-wnjw;(9+fEEY>
z0sue@>*qsIa@F_PsrpyGAEPAIXwLvo0Jv4Eu^FkRtj#EXga80wS^z-iIRN;oY-P#-
zAQ=e&iU<I}umQj@zE%|MrW~k59X#Lze9J49b}K7&1~$M~U8c4Jtf{6cF+B<ZJHS{U
z&(KrPKh%ZC>Jg1HyHie+Z5uPSf<^}{%zlFGDLZeJYm}|iV$ObOq~Bn1Eg-)D@=ual
zHaNRC2YjR+bu|LG(4%_1?9%?wIE($LC{-)N9@0nNTAZs#EDy#=U41Xzd|SJm{`)wc
zzV_|yyB{+~Ui8ezqoLY)ec$O@D#&a7{Fr|vgjf-5l-ALb?%Pz%nXv+~H{DTfWrwZ(
z@)u^jwfJ&_0)1<&K|=91x3L>FYxlj^n)wdr?=V0`U-478PDW*WeT4yWQjQz-=Q<Os
z<Q(r#645}869#lzgG=|A9Ny|XGKs()OkQ-vFbkOXRhMN?&=8v_12w2Qth`Vp&^;E-
z@lOQ42(uDG>Fk=x<ovTe#d_Bl^5dhKg@fqoWD2Sym6+reMsi2m@e!2pGg>VW>V{lF
zhove=loA^@>&`W`X1F59I#O6;5lF8M7wd8wS`al<PmO@u$LM`Sx^vF_RQZhNQ|ZdA
z(Oy2H(^?|}e8IznLY<&-1-DycO<of?4P&$ho9Tw8i5V+Nw@5Q)MP#!1k5?^olVTOE
zmN`24tjO!+N{U3>DDsM3>5zzNW;)LY{@rcUljQ@Y8oQMtqh;~%KeiV<61@s0mxc4e
z*Wn&Q)m7uy=J3xXQ#*3Z(H(b~A<CT>KR6Dw2+exW44}n@f{DS9sto>dF##9ajcOHc
z9zJ7Lbe*szUKiuIXvvYIyH=7*+ikk%8vVOy8!HIkR|xp83Q$U3uI4U~%Hn<2)U;o~
zl8=-{lqPai<|qrfN$tfOLQtAtD#-f=I&ol#@aXR#ovqum3O)_>%nlAE_@?c|U)g!C
zT8J2XTpdX~?xVK$*%I$q&il9!T82J(s!E|lDP1I=)s$m(TNLU)zgLhCJ;QA`P4LE_
z>ZESFeXF*=c1q{D2YGbFfP;pcPD^)u${P$UeD+xNS(`J7#Mv#le9)QqK<MVYToCJb
zn&t^UF>Pl&N+-vcfbSs;hvUd?MJ<UvN+<j6VS2P_yZ03LXdb~nLl)%k=qOvj7;Y3j
zt$lM6vlKV{t@241yu489v%0^0U&gz6ho<%F2hgi^o{uORBe@BQ9;{I5pvh{|r6K7X
zt&Uqo;A&s1CD-2`n@8mcE?PEL-JKr#mttvp7*=q(LyQX<lB7!<5N-G$YB$^^rPidN
z!34tC&7v)#S&xdssMsS2O5NsPu`?=lM0cQ8j7!LMZk@L;`PH^hje_(f?dxtU?SnSe
zX<shJAryf>nKL8`sv&oI`=PuE0;{^2A;E~PA3j|}rW3ymK_Xlw>mhdmea6;4|65u8
zDL2N6<UQQa^{pX|8x2ju!(owJYj@#uGPb7oGv{WVu~(kJZXFvbKj(t)sANqHuyC#q
z^vB=UIqtQ42MktLaR+8!xzP6?oAhK>RB=Zvm!XbkwkOI$a1#8Y@w{fcnccy~38F8`
zfr#$2@sr??fsu0NF`Z#M__z?nuK!)=NbV1t-xU7v+sn5OE=`N{wk-8u+9icu0(sX<
zR|~s^UCzNx2e2Kt7xq3J>J^AAmbAGcd_2yA0Zld<!3?zLmdw1-bGW$Cg{vFo!}Q0J
z^69#!?U_%YFHM<QUShU1$F29s$EL8m75*{|fy1jGbiu<v@GPTsCtlE6uc0udJ*=R;
zNjHMR?v)EbX!8HVbqS=5c_Qf-o>9<NzDUrjQ{w&1FYjwWpF$WXyZ6amWw`nHU;~0*
z+9#TF)YBeo`g$=PlCE)xaWf_Y@$~8+SJJ7w=TFbRgRp0zl^~}Y=P}mVbMANSUAYs(
zpKQcwg>1?jV>VP4?Gz6CG;bNb+v<b<rICs#_r>uHuD#%j_kWuCfpr2}@ono&^N}PJ
z??49De>?PbzZpJ7g9(dw(z4-Iff`(=fvNQiyVIYzsC;RD$qFz;a;#nqnv08H*qZXn
zH8y*&ajH6t|2@Xq>W{i@SH~_n^}=$L-vY+)4P#Iv88i$fk)|ZT0qKCUMLO9cokQ)N
tF!uHslmi@z#2}HGT-n0^2!1|EjiIOgy<oXFC`>5;u)cvltzHq?e*%LmG)Mpd

literal 0
HcmV?d00001

diff --git a/src/sigal/gallery.py b/src/sigal/gallery.py
index 8adfabbe..5f1004f9 100644
--- a/src/sigal/gallery.py
+++ b/src/sigal/gallery.py
@@ -53,11 +53,13 @@
     copy,
     get_mime,
     get_mod_date,
+    is_valid_html5_audio,
     is_valid_html5_video,
     read_markdown,
     should_reprocess_album,
     url_from_path,
 )
+from .audio import process_audio
 from .video import process_video
 from .writer import AlbumListPageWriter, AlbumPageWriter
 
@@ -345,6 +347,32 @@ def date(self):
         return self._get_file_date()
 
 
+class Audio(Media):
+    """Gather all informations on an audio file."""
+
+    type = "audio"
+
+    def __init__(self, filename, path, settings):
+        super().__init__(filename, path, settings)
+        self.mime = get_mime(self.src_ext)
+
+    @cached_property
+    def date(self):
+        """The date from the Date metadata if available, or from the file date."""
+        if "date" in self.meta:
+            try:
+                self.logger.debug(
+                    "Reading date from image metadata : %s", self.src_filename
+                )
+                return datetime.fromisoformat(self.meta["date"][0])
+            except Exception:
+                self.logger.debug(
+                    "Reading date from image metadata failed : %s", self.src_filename
+                )
+        # If no date is found in the metadata, return the file date.
+        return self._get_file_date()
+
+
 class Album:
     """Gather all informations on an album.
 
@@ -403,6 +431,8 @@ def __init__(self, path, settings, dirnames, filenames, gallery):
                 media = Image(f, self.path, settings)
             elif ext.lower() in settings["video_extensions"]:
                 media = Video(f, self.path, settings)
+            elif ext.lower() in settings["audio_extensions"]:
+                media = Audio(f, self.path, settings)
 
             # Allow modification of the media, including overriding the class
             # type for the media.
@@ -964,6 +994,8 @@ def process_file(media):
         processor = process_image
     elif media.type == "video":
         processor = process_video
+    elif media.type == "audio":
+        processor = process_audio
 
     # Allow overriding of the processor
     result = signals.process_file.send(media, processor=processor)
diff --git a/src/sigal/settings.py b/src/sigal/settings.py
index 60cee8b3..752a77d6 100644
--- a/src/sigal/settings.py
+++ b/src/sigal/settings.py
@@ -87,6 +87,7 @@
     "video_format": "webm",
     "video_always_convert": False,
     "video_size": (480, 360),
+    "audio_extensions": [".m4a", ".mp3", ".ogg", ".wav"],
     "watermark": "",
     "webm_options": ["-crf", "10", "-b:v", "1.6M", "-qmin", "4", "-qmax", "63"],
     "webm_options_second_pass": None,
@@ -121,6 +122,8 @@ def get_thumb(settings, filename):
 
     if ext.lower() in settings["video_extensions"]:
         ext = ".jpg"
+    if ext.lower() in settings["audio_extensions"]:
+        ext = ".png"  # extension of sigal.audio.AUDIO_THUMB_FILE
     return join(
         path,
         settings["thumb_dir"],
diff --git a/src/sigal/utils.py b/src/sigal/utils.py
index dc95f7b6..89856c66 100644
--- a/src/sigal/utils.py
+++ b/src/sigal/utils.py
@@ -35,6 +35,7 @@
 logger = logging.getLogger(__name__)
 MD = None
 VIDEO_MIMES = {".mp4": "video/mp4", ".webm": "video/webm", ".ogv": "video/ogg"}
+AUDIO_MIMES = {".m4a": "audio/m4a", ".mp3": "audio/mpeg", ".ogg": "audio/ogg", ".wav": "audio/x-wav"}
 
 
 class Devnull:
@@ -146,9 +147,17 @@ def is_valid_html5_video(ext):
     return ext in VIDEO_MIMES.keys()
 
 
+def is_valid_html5_audio(ext):
+    """Checks if ext is a supported HTML5 video."""
+    return ext in AUDIO_MIMES.keys()
+
+
 def get_mime(ext):
     """Returns mime type for extension."""
-    return VIDEO_MIMES[ext]
+    mime_type = VIDEO_MIMES.get(ext) or AUDIO_MIMES.get(ext)
+    if not mime_type:
+        raise RuntimeError(f"Could not figure mime type, unknown file extension: {ext}")
+    return mime_type
 
 
 def init_plugins(settings):