From 69ec048bc92a2bfaf6e5db89998a1dc33b8e74ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gabor=20K=C3=B6rber?= <gab@g4b.org>
Date: Sat, 20 Jan 2024 22:36:30 +0100
Subject: [PATCH] code: adding mp3 duration to lock buttons client side

---
 Cargo.lock            | 14 ++++++++++++--
 Cargo.toml            |  2 ++
 src/soundclips.rs     | 43 +++++++++++++++++++++++++++++++++++++++++++
 templates/index.jinja | 41 ++++++++++++++++++++++++++++++++++++++---
 4 files changed, 95 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 6bee7ff..1beb0ef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -560,9 +560,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.147"
+version = "0.2.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
 
 [[package]]
 name = "libloading"
@@ -689,6 +689,15 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "mp3-duration"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "348bdc7300502f0801e5b57c448815713cd843b744ef9bda252a2698fdf90a0f"
+dependencies = [
+ "thiserror",
+]
+
 [[package]]
 name = "ndk"
 version = "0.7.0"
@@ -1145,6 +1154,7 @@ dependencies = [
  "log",
  "minijinja",
  "minimp3",
+ "mp3-duration",
  "regex",
  "rodio",
  "serde",
diff --git a/Cargo.toml b/Cargo.toml
index b0d0f36..8e0d822 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,8 +11,10 @@ axum = "0.6.18"
 axum-template = { version = "0.19.0", features = ["minijinja"] }
 cpal = "0.15.2"
 log = "0.4.20"
+# metadata = "0.1.8"
 minijinja = { version = "1.0.3", features = ["loader"] }
 minimp3 = "0.5.1"
+mp3-duration = "0.1.10"
 regex = "1.9.0"
 rodio = "0.17.1"
 serde = { version = "1.0.171", features = ["derive"] }
diff --git a/src/soundclips.rs b/src/soundclips.rs
index 91dfbd3..4023caa 100644
--- a/src/soundclips.rs
+++ b/src/soundclips.rs
@@ -6,9 +6,11 @@ use std::fs;
 
 #[derive(Debug, Clone, Serialize)]
 pub struct SoundClip {
+    title: String,
     hash: String,
     file_name: String,
     directory: String,
+    length: Option<u64>,
 }
 
 impl fmt::Display for SoundClip {
@@ -23,8 +25,11 @@ impl fmt::Display for SoundClip {
 
 impl SoundClip {
     pub fn new(file_name: String, directory: String) -> Self {
+        let full_file_name = format!("{}/{}", directory, file_name);
         Self {
+            title: normalize_filename(&file_name),
             hash: encode_filename(&file_name),
+            length: get_sound_clip_length(&full_file_name),
             file_name: file_name,
             directory: directory,
         }
@@ -49,6 +54,44 @@ fn encode_filename(file_name: &str) -> String {
     hash_hex
 }
 
+/* // needs metadata to install ffmpeg.
+fn get_sound_clip_length(file_path: &str) -> Option<u64> {
+    if let Ok(meta) = metadata::media_file::MediaFileMetadata::new(&std::path::Path::new(file_path))
+    {
+        return Some(meta._duration);
+    }
+    return None;
+}
+*/
+
+fn get_sound_clip_length(file_path: &str) -> Option<u64> {
+    let path = std::path::Path::new(file_path);
+    if let Ok(duration) = mp3_duration::from_path(&path) {
+        if let Ok(millis) = duration.as_millis().try_into() {
+            return Some(millis);
+        }
+        return Some(duration.as_secs() * 1000 + 1);
+    }
+    return None;
+}
+
+fn normalize_filename(input: &str) -> String {
+    // List of extensions to be removed
+    let extensions = [".mp3", ".wav", ".flac", ".ogg"];
+
+    // Remove the extension if it matches any in the list
+    let without_extension = extensions.iter().fold(input.to_string(), |acc, &ext| {
+        if acc.ends_with(ext) {
+            acc.trim_end_matches(ext).to_string()
+        } else {
+            acc
+        }
+    });
+
+    // Replace all underscores with spaces
+    without_extension.replace("_", " ")
+}
+
 pub fn scan_directory_for_clips(directory: &str, extensions: &[&str]) -> Option<Vec<SoundClip>> {
     let mut sound_clips = Vec::new();
 
diff --git a/templates/index.jinja b/templates/index.jinja
index 235fde5..0419c6f 100644
--- a/templates/index.jinja
+++ b/templates/index.jinja
@@ -47,12 +47,42 @@
       }
     }
 
-    function play(hash) {
+    var disableTimeout;
+    var none = null;
+
+    function play(hash, timeout) {
+
+      if (timeout && timeout != "none") {
+        // Add the "disabled" class to all clip buttons
+        $('.clip').addClass('disabled');
+
+        // Add "primary" and "loading" classes to the clicked button
+        var clickedButton = $('#clip-' + hash);
+        clickedButton.addClass('double loading positive');
+
+        // Clear any existing timeout to avoid conflicts
+        clearTimeout(disableTimeout);
+
+        // Set a new timeout
+        disableTimeout = setTimeout(function () {
+          $('.clip').removeClass('disabled');
+          clickedButton.removeClass('double loading positive');
+        }, timeout);
+      }
+
       fetch("/play/" + hash);
     }
 
     function stop() {
       fetch("/stop");
+
+      // Clear the timeout
+      clearTimeout(disableTimeout);
+
+      // Remove the "disabled" class from all clip buttons
+      // and the "primary" and "loading" classes from any button that might have them
+      $('.clip').removeClass('disabled');
+      $('.clip').removeClass('double loading positive');
     }
   </script>
   <style type="text/css">
@@ -61,6 +91,10 @@
       /* Define a CSS variable for the width */
     }
 
+    .ui.ui.ui.ui.ui.ui.loading.button {
+      color: black !important;
+    }
+
     .left-column {
       width: var(--left-column-width) !important;
       height: 100%;
@@ -110,8 +144,9 @@
 
       <div class="ui wrapped compact wrapping spaced /**buttons">
         {% for clip in clips %}
-        <button class="ui {{ loop.cycle('red', 'blue', 'green', 'violet', 'orange') }} basic button"
-          onclick="play('{{clip.hash}}')">{{clip.file_name}}</button>
+        <button id="clip-{{clip.hash}}"
+          class="ui {{ loop.cycle('red', 'blue', 'green', 'violet', 'orange') }} basic button clip"
+          onclick="play('{{clip.hash}}', {{clip.length}})">{{clip.title}}</button>
         {% endfor %}
 
       </div>