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 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 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):