]> gitweb.mndrdr.org Git - arelpe.git/commitdiff
Adding audio support to the platform
authorAidan Cornelius-Bell <[email protected]>
Fri, 17 Jan 2025 01:56:49 +0000 (12:26 +1030)
committerAidan Cornelius-Bell <[email protected]>
Fri, 17 Jan 2025 01:56:49 +0000 (12:26 +1030)
23 files changed:
Gemfile
Gemfile.lock
app/assets/stylesheets/application.css
app/controllers/audios_controller.rb [new file with mode: 0644]
app/models/audio.rb [new file with mode: 0644]
app/models/post.rb
app/views/audios/_form.html.erb [new file with mode: 0644]
app/views/audios/edit.html.erb [new file with mode: 0644]
app/views/audios/feed.rss.erb [new file with mode: 0644]
app/views/audios/index.html.erb [new file with mode: 0644]
app/views/audios/new.html.erb [new file with mode: 0644]
app/views/audios/show.html.erb [new file with mode: 0644]
app/views/layouts/_navigation_buttons.html.erb
app/views/pubview/post.html.erb
app/views/shared/_audio_player.html.erb [new file with mode: 0644]
config/application.rb
config/environments/development.rb
config/routes.rb
db/migrate/20250116235629_create_audios.rb [new file with mode: 0644]
db/migrate/20250117001201_create_active_storage_tables.active_storage.rb [new file with mode: 0644]
db/migrate/20250117001421_add_storage_to_audios.rb [new file with mode: 0644]
db/migrate/20250117002947_update_audio_duration_constraint.rb [new file with mode: 0644]
db/schema.rb

diff --git a/Gemfile b/Gemfile
index 7fd6a65e5992221aba329bddadefc2440ad1e257..4acd5f8ed0933846d2c569687a63e5f0b6d07a6d 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -24,6 +24,8 @@ gem "dotenv"
 gem "hcaptcha"
 # subscribers
 gem "stripe"
+# for audio conversion
+gem "streamio-ffmpeg"
 # Adding this because: ostruct.rb was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.5.0.
 gem 'ostruct'
 # Scheduling
index 00aa9c9d3b5d934322888194b25a19f3b071ed98..2d948c14e92fe6d05bf86378b48c0796238b38d0 100644 (file)
@@ -98,7 +98,7 @@ GEM
       logger (~> 1.5)
     chronic (0.10.2)
     chunky_png (1.4.0)
-    concurrent-ruby (1.3.4)
+    concurrent-ruby (1.3.5)
     connection_pool (2.5.0)
     crass (1.0.6)
     csv (3.3.2)
@@ -201,7 +201,7 @@ GEM
     orm_adapter (0.5.0)
     ostruct (0.6.1)
     parallel (1.26.3)
-    parser (3.3.6.0)
+    parser (3.3.7.0)
       ast (~> 2.4.1)
       racc
     psych (5.2.2)
@@ -254,7 +254,7 @@ GEM
       zeitwerk (~> 2.6)
     rainbow (3.1.1)
     rake (13.2.1)
-    rdoc (6.10.0)
+    rdoc (6.11.0)
       psych (>= 4.0.0)
     redcarpet (3.6.0)
     regexp_parser (2.10.0)
@@ -314,6 +314,8 @@ GEM
       actionpack (>= 6.1)
       activesupport (>= 6.1)
       sprockets (>= 3.0.0)
+    streamio-ffmpeg (3.0.2)
+      multi_json (~> 1.8)
     stringio (3.1.2)
     stripe (5.55.0)
     stripe-ruby-mock (3.1.0)
@@ -324,7 +326,7 @@ GEM
     timeout (0.4.3)
     tzinfo (2.0.6)
       concurrent-ruby (~> 1.0)
-    unicode-display_width (3.1.3)
+    unicode-display_width (3.1.4)
       unicode-emoji (~> 4.0, >= 4.0.4)
     unicode-emoji (4.0.4)
     uri (1.0.2)
@@ -379,6 +381,7 @@ DEPENDENCIES
   rubocop-rails-omakase
   selenium-webdriver
   sprockets-rails
+  streamio-ffmpeg
   stripe
   stripe-ruby-mock (~> 3.1.0)
   tzinfo-data
index 014636b8bc275c5b425624ebc10880957f34281b..75155dd8e5a67c1de4e1d5172e4b1e0947f8d2ed 100644 (file)
@@ -391,6 +391,27 @@ footer p {
     margin: auto;
 }
 
+/* Podcast / audio views */
+.audio-player {
+    margin: 2em auto;
+    padding: 1em;
+    background: var(--accent-b);
+    border-radius: 4px;
+}
+.audio-player audio {
+    width: 100%;
+}
+.audio-player .duration {
+    color: var(--filter-button-text);
+    font-size: 0.9em;
+    margin-top: 0.5em;
+    text-align: right;
+}
+
+.audio-player .use-description {
+    margin: 0 0 -2em 0;
+}
+
 .promo {
     border: 1px solid var(--code-text);
     border-radius: 3px;
diff --git a/app/controllers/audios_controller.rb b/app/controllers/audios_controller.rb
new file mode 100644 (file)
index 0000000..cbe1739
--- /dev/null
@@ -0,0 +1,62 @@
+class AudiosController < ApplicationController
+  include AdminAuthenticatable
+  before_action :set_audio, only: %i[ show edit update destroy ]
+  skip_before_action :authenticate_admin!, only: [:feed]
+
+  def index
+    @audios = Audio.order(published_at: :desc).page(params[:page]).per(20)
+  end
+
+  def show
+  end
+
+  def new
+    @audio = Audio.new
+    @audio.post_id = params[:post_id] if params[:post_id]
+  end
+
+  def edit
+  end
+
+  def create
+    @audio = Audio.new(audio_params)
+
+    if @audio.save
+      redirect_to @audio, notice: "Audio was successfully created."
+    else
+      render :new, status: :unprocessable_entity
+    end
+  end
+
+  def update
+    if @audio.update(audio_params)
+      redirect_to @audio, notice: "Audio was successfully updated."
+    else
+      render :edit, status: :unprocessable_entity
+    end
+  end
+
+  def destroy
+    @audio.destroy!
+    redirect_to audios_url, notice: "Audio was successfully destroyed."
+  end
+
+  def feed
+    @audios = Audio.published.order(published_at: :desc).limit(50)
+
+    respond_to do |format|
+      format.rss do
+        render layout: false, content_type: 'application/rss+xml'
+      end
+    end
+  end
+
+  private
+    def set_audio
+      @audio = Audio.find(params[:id])
+    end
+
+    def audio_params
+      params.require(:audio).permit(:title, :description, :file, :duration_in_seconds, :published_at, :post_id)
+    end
+end
diff --git a/app/models/audio.rb b/app/models/audio.rb
new file mode 100644 (file)
index 0000000..110bee3
--- /dev/null
@@ -0,0 +1,111 @@
+class Audio < ApplicationRecord
+  belongs_to :post, optional: true
+  has_one_attached :file
+
+  validates :title, presence: true
+  validates :description, presence: true
+  validates :published_at, presence: true
+  validates :file, presence: true, on: :create
+  validate :acceptable_file_type, if: -> { file.attached? }
+
+  before_validation :set_published_at, on: :create
+  after_create_commit :process_audio_file
+
+  scope :published, -> { where("published_at <= ?", Time.current) }
+
+  def duration_formatted
+    return nil unless duration_in_seconds
+
+    minutes = duration_in_seconds / 60
+    seconds = duration_in_seconds % 60
+    format("%02d:%02d", minutes, seconds)
+  end
+
+  def rss_duration
+    return nil unless duration_in_seconds
+
+    hours = duration_in_seconds / 3600
+    minutes = (duration_in_seconds % 3600) / 60
+    seconds = duration_in_seconds % 60
+
+    if hours > 0
+      format("%02d:%02d:%02d", hours, minutes, seconds)
+    else
+      format("%02d:%02d", minutes, seconds)
+    end
+  end
+
+  def needs_conversion?
+    return false unless file.attached?
+    content_type = file.content_type.to_s
+    content_type.include?('m4a') ||
+      content_type.include?('aac') ||
+      content_type.include?('mp4') ||
+      !content_type.include?('mpeg')
+  end
+
+  private
+
+  def set_published_at
+    self.published_at ||= Time.current
+  end
+
+  def acceptable_file_type
+    accepted_types = %w[
+      audio/mpeg audio/mp3 audio/ogg audio/wav audio/x-m4a
+      audio/aac audio/mp4 application/mp4 audio/x-hx-aac-adts
+    ]
+
+    unless file.content_type.in?(accepted_types)
+      errors.add(:file, 'must be an MP3, M4A, OGG, or WAV file')
+    end
+  end
+
+  def process_audio_file
+    return unless file.attached?
+
+    Rails.logger.info "Processing audio file: #{file.filename}"
+    Rails.logger.info "Content type: #{file.content_type}"
+
+    file.open do |tempfile|
+      begin
+        movie = FFMPEG::Movie.new(tempfile.path)
+        Rails.logger.info "FFMPEG opened file successfully"
+
+        # Set duration
+        update_column(:duration_in_seconds, movie.duration.to_i)
+        Rails.logger.info "Duration set to: #{duration_in_seconds}"
+
+        # Convert non-MP3 files to MP3
+        if needs_conversion?
+          Rails.logger.info "Converting file to MP3"
+          mp3_path = File.join(Dir.tmpdir, "#{SecureRandom.uuid}.mp3")
+
+          # Convert to mp3 with higher quality settings
+          movie.transcode(mp3_path, %w[-acodec libmp3lame -ab 256k -ar 44100 -map_metadata 0 -id3v2_version 3])
+          Rails.logger.info "File converted successfully"
+
+          # Attach the converted file
+          file.attach(
+            io: File.open(mp3_path),
+            filename: file.filename.to_s.sub(/\.[^.]+\z/, '.mp3'),
+            content_type: 'audio/mpeg'
+          )
+          Rails.logger.info "Converted file attached successfully"
+
+          # Clean up
+          File.unlink(mp3_path) if File.exist?(mp3_path)
+        end
+
+      rescue FFMPEG::Error => e
+        Rails.logger.error "FFMPEG Error: #{e.message}"
+        Rails.logger.error e.backtrace.join("\n")
+        errors.add(:file, "could not be processed: #{e.message}")
+      rescue StandardError => e
+        Rails.logger.error "Standard Error: #{e.message}"
+        Rails.logger.error e.backtrace.join("\n")
+        errors.add(:file, "encountered an error during processing: #{e.message}")
+      end
+    end
+  end
+end
index f4d0cc374619d7c007491024f8a8951587188f75..6a54d0c12fb64f21638793ad2d20cd7993099abe 100644 (file)
@@ -16,6 +16,8 @@ class Post < ApplicationRecord
   scope :dispatches, -> { where(post_type: 'dispatch') }
   scope :bookmarks, -> { where(post_type: 'bookmark') }
 
+  has_one :audio, dependent: :nullify
+
   def self.get_posts_and_bookmarks_with_pagination(page, per_page, filter)
     posts = case filter
     when 'posts'
diff --git a/app/views/audios/_form.html.erb b/app/views/audios/_form.html.erb
new file mode 100644 (file)
index 0000000..dab5840
--- /dev/null
@@ -0,0 +1,43 @@
+<%# app/views/audios/_form.html.erb %>
+<%= form_with(model: audio, local: true) do |form| %>
+  <% if audio.errors.any? %>
+    <div class="error-messages">
+      <h2><%= pluralize(audio.errors.count, "error") %> prohibited this audio from being saved:</h2>
+      <ul>
+        <% audio.errors.full_messages.each do |message| %>
+          <li><%= message %></li>
+        <% end %>
+      </ul>
+    </div>
+  <% end %>
+
+  <div class="field">
+    <%= form.label :title %>
+    <%= form.text_field :title %>
+  </div>
+
+  <div class="field">
+    <%= form.label :description %>
+    <%= form.text_area :description %>
+  </div>
+
+  <div class="field">
+    <%= form.label :file %>
+    <%= form.file_field :file, accept: 'audio/mpeg,audio/mp3,audio/ogg,audio/wav,audio/x-m4a,audio/aac' %>
+    <small>Supports MP3, M4A, OGG, and WAV files. M4A files will be automatically converted to MP3.</small>
+  </div>
+
+  <div class="field">
+    <%= form.label :published_at %>
+    <%= form.datetime_local_field :published_at %>
+  </div>
+
+  <div class="field">
+    <%= form.label :post_id, "Related Post" %>
+    <%= form.collection_select :post_id, Post.where(post_type: :dispatch).order(published_at: :desc), :id, :title, { include_blank: "None" } %>
+  </div>
+
+  <div class="actions">
+    <%= form.submit %> or <%= link_to "Cancel", audios_path %>
+  </div>
+<% end %>
diff --git a/app/views/audios/edit.html.erb b/app/views/audios/edit.html.erb
new file mode 100644 (file)
index 0000000..2fe2c37
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="container">
+  <% content_for :title, "edit audio episode" %>
+  <h1>Edit Audio Episode</h1>
+  <%= link_to "Back to audio", audio_path(@audio), class: "button" %>
+</div>
+
+<div class="post">
+  <div class="container">
+    <%= render "form", audio: @audio %>
+  </div>
+</div>
diff --git a/app/views/audios/feed.rss.erb b/app/views/audios/feed.rss.erb
new file mode 100644 (file)
index 0000000..79e4700
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+  <channel>
+    <title><%= Rails.application.config.site_name %> Podcast</title>
+    <link><%= root_url %></link>
+    <language>en-us</language>
+    <copyright>© <%= Time.current.year %> <%= Rails.application.config.site_name %></copyright>
+    <itunes:author><%= Rails.application.config.site_author %></itunes:author>
+    <description><%= Rails.application.config.podcast_description %></description>
+    <itunes:type>episodic</itunes:type>
+    <itunes:owner>
+      <itunes:name><%= Rails.application.config.site_author %></itunes:name>
+      <itunes:email><%= Rails.application.config.admin_email %></itunes:email>
+    </itunes:owner>
+    <itunes:image href="<%= Rails.application.config.podcast_image_url %>"/>
+    <itunes:category text="<%= Rails.application.config.podcast_category %>"/>
+
+    <% @audios.each do |audio| %>
+      <% if audio.file.attached? %>
+        <item>
+          <title><%= audio.title %></title>
+          <description><![CDATA[<%= audio.description %>]]></description>
+          <pubDate><%= audio.published_at.rfc2822 %></pubDate>
+          <enclosure
+            url="<%= rails_blob_url(audio.file) %>"
+            length="<%= audio.file.byte_size %>"
+            type="audio/mpeg"/>
+          <guid isPermaLink="false"><%= audio_url(audio) %></guid>
+          <itunes:duration><%= audio.rss_duration %></itunes:duration>
+          <itunes:summary><![CDATA[<%= audio.description %>]]></itunes:summary>
+          <% if audio.post.present? %>
+            <link><%= post_url(audio.post) %></link>
+          <% end %>
+        </item>
+      <% end %>
+    <% end %>
+  </channel>
+</rss>
diff --git a/app/views/audios/index.html.erb b/app/views/audios/index.html.erb
new file mode 100644 (file)
index 0000000..9e1e422
--- /dev/null
@@ -0,0 +1,45 @@
+<div class="container">
+  <% content_for :title, "audio episodes" %>
+  <h1>Audio Episodes</h1>
+  <%= link_to "New Audio Episode", new_audio_path, class: "button" %>
+  <%= link_to "Home", root_path, class: "button" %>
+</div>
+
+<%= render partial: "layouts/alert" %>
+
+<div class="post">
+  <div class="container">
+    <table>
+      <thead>
+        <tr>
+          <th>Title</th>
+          <th>Duration</th>
+          <th>Published</th>
+          <th>Related Post</th>
+          <th>Actions</th>
+        </tr>
+      </thead>
+      <tbody>
+        <% @audios.each do |audio| %>
+          <tr>
+            <td><%= audio.title %></td>
+            <td><%= audio.duration_formatted %></td>
+            <td><%= audio.published_at.strftime("%Y-%m-%d %H:%M") %></td>
+            <td>
+              <% if audio.post %>
+                <%= link_to audio.post.title, post_path(audio.post) %>
+              <% else %>
+                <span class="quiet">None</span>
+              <% end %>
+            </td>
+            <td>
+              <%= link_to "View", audio_path(audio), class: "button small" %>
+              <%= link_to "Edit", edit_audio_path(audio), class: "button small" %>
+              <%= link_to "Delete", audio_path(audio), method: :delete, data: { confirm: "Are you sure you want to delete this audio episode?" }, class: "button small danger" %>
+            </td>
+          </tr>
+        <% end %>
+      </tbody>
+    </table>
+  </div>
+</div>
diff --git a/app/views/audios/new.html.erb b/app/views/audios/new.html.erb
new file mode 100644 (file)
index 0000000..494ee65
--- /dev/null
@@ -0,0 +1,11 @@
+<div class="container">
+  <% content_for :title, "new audio episode" %>
+  <h1>New Audio Episode</h1>
+  <%= link_to "Back to list", audios_path, class: "button" %>
+</div>
+
+<div class="post">
+  <div class="container">
+    <%= render "form", audio: @audio %>
+  </div>
+</div>
diff --git a/app/views/audios/show.html.erb b/app/views/audios/show.html.erb
new file mode 100644 (file)
index 0000000..611b038
--- /dev/null
@@ -0,0 +1,38 @@
+<div class="container">
+  <% content_for :title, @audio.title %>
+  <h1><%= @audio.title %></h1>
+  <%= link_to "Edit", edit_audio_path(@audio), class: "button" %>
+  <%= link_to "Back", audios_path, class: "button" %>
+</div>
+
+<div class="post">
+  <div class="container">
+    <div class="audio-player">
+      <% if @audio.needs_conversion? %>
+        <p class="processing-notice">Audio file is being converted to MP3 format. Please refresh in a moment.</p>
+      <% else %>
+        <audio controls preload="metadata">
+          <source src="<%= rails_blob_path(@audio.file) %>" type="audio/mpeg">
+          Your browser does not support the audio element.
+        </audio>
+        <% if @audio.duration_in_seconds %>
+          <div class="duration">Duration: <%= @audio.duration_formatted %></div>
+        <% end %>
+      <% end %>
+    </div>
+
+    <h2>Details</h2>
+    <dl>
+      <dt>Published At</dt>
+      <dd><%= @audio.published_at.strftime("%B %d, %Y at %I:%M %p") %></dd>
+
+      <dt>Description</dt>
+      <dd><%= @audio.description %></dd>
+
+      <% if @audio.post.present? %>
+        <dt>Related Post</dt>
+        <dd><%= link_to @audio.post.title, post_path(@audio.post) %></dd>
+      <% end %>
+    </dl>
+  </div>
+</div>
index f8b299827fc8bc1b734b525196788fb94c38c3ab..223bce8275f58232fe7da6d6a43f83c819301cf2 100644 (file)
@@ -14,6 +14,7 @@
 
   <% if current_user&.admin? %>
   <%= link_to "Manage posts", posts_path, class: "button" %>
+  <%= link_to "Manage audios", audios_path, class: "button" %>
   <%= link_to "Manage pages", pages_path, class: "button" %>
   <%= link_to "Manage API keys", api_keys_path, class: "button" %>
   <%= link_to "Manage users", user_manager_index_path, class: "button" %>
index e3a39239aed7020a14f77a7d8ac7ca8bd502649e..223912c37bebb6faf5c0d286d0e6a9168d4968d5 100644 (file)
             <p class="lead"><%= @promo_strings.sample %> <%= link_to "Start right now!", join_path, class: "lead-button" %></p>
         </div>
     <% end %>
+
+    <% if @post.audio.present? && @post.audio.file.attached? %>
+        <%= render partial: "shared/audio_player", locals: { audio: @post.audio } %>
+    <% end %>
+
        <%= raw @rendered_content %>
 
        <% if @post.bookmark? && @post.summary? %>
diff --git a/app/views/shared/_audio_player.html.erb b/app/views/shared/_audio_player.html.erb
new file mode 100644 (file)
index 0000000..4942108
--- /dev/null
@@ -0,0 +1,12 @@
+<% if audio && audio.file.attached? %>
+  <div class="audio-player">
+    <audio controls preload="metadata">
+      <source src="<%= rails_blob_path(audio.file) %>" type="<%= audio.file.content_type %>">
+      We tried to show you an audio version of this post but your browser does not support the audio element.
+    </audio>
+    <p class="use-description">Listen to this post.</p>
+    <% if audio.duration_in_seconds %>
+      <div class="duration">Duration: <%= audio.duration_formatted %></div>
+    <% end %>
+  </div>
+<% end %>
index 72952459383ab2c44ae476332971bd1a4d6d185d..69d13b3e66d6bd73ffb6028b0163d06df4eb600d 100644 (file)
@@ -37,5 +37,12 @@ module Arelpe
     config.time_zone = "Australia/Adelaide"
     # config.eager_load_paths << Rails.root.join("extras")
     config.active_support.to_time_preserves_timezone = :zone
+
+    config.site_name = "mind reader"
+    config.site_author = "Aidan Cornelius-Bell"
+    config.podcast_description = "Anti-capitalist dispatches for individualistic times."
+    config.podcast_category = "Society & Culture"
+    config.podcast_image_url = "https://mndrdr.org/podcast-cover.jpg"
+    config.admin_email = "[email protected]"
   end
 end
index 74d7105a075a72832cb6c050722039785f5194a7..7be03bd0d10c3bf76f564290963f47e03a4084a2 100644 (file)
@@ -17,6 +17,9 @@ Rails.application.configure do
   # Enable server timing.
   config.server_timing = true
 
+  # Do jobs
+  config.active_job.queue_adapter = :async
+
   # Enable/disable caching. By default caching is disabled.
   # Run rails dev:cache to toggle caching.
   if Rails.root.join("tmp/caching-dev.txt").exist?
@@ -42,7 +45,7 @@ Rails.application.configure do
   config.action_mailer.perform_caching = false
 
   config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
-  
+
   config.action_mailer.delivery_method = :letter_opener
   config.action_mailer.perform_deliveries = true
 
@@ -75,7 +78,7 @@ Rails.application.configure do
 
   # Raise error when a before_action's only/except options reference missing actions.
   config.action_controller.raise_on_missing_callback_actions = true
-  
+
   # Turn off captcha stuff for localhost
   config.hcaptcha_enabled = false
 
index f4dcd09e54fa7cd58d888b6244d1353cc735ede0..b96708f6d0362d55731b473eaa301bc1f9b93541 100644 (file)
@@ -43,6 +43,10 @@ Rails.application.routes.draw do
     end
   end
 
+  resources :audios do
+    get :feed, on: :collection, defaults: { format: 'rss' }
+  end
+
   get '/feed', to: 'pubview#rss', as: 'rss', defaults: { format: 'rss' }
   get '/feed/dispatches', to: 'pubview#dispatches_rss', as: 'dispatches_rss', defaults: { format: 'rss' }
   get '/join', to: "pubview#join"
diff --git a/db/migrate/20250116235629_create_audios.rb b/db/migrate/20250116235629_create_audios.rb
new file mode 100644 (file)
index 0000000..39519d1
--- /dev/null
@@ -0,0 +1,14 @@
+class CreateAudios < ActiveRecord::Migration[8.0]
+  def change
+    create_table :audios do |t|
+      t.string :title
+      t.text :description
+      t.string :audio_url
+      t.integer :duration_in_seconds
+      t.datetime :published_at
+      t.references :post, null: false, foreign_key: true
+
+      t.timestamps
+    end
+  end
+end
diff --git a/db/migrate/20250117001201_create_active_storage_tables.active_storage.rb b/db/migrate/20250117001201_create_active_storage_tables.active_storage.rb
new file mode 100644 (file)
index 0000000..6bd8bd0
--- /dev/null
@@ -0,0 +1,57 @@
+# This migration comes from active_storage (originally 20170806125915)
+class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
+  def change
+    # Use Active Record's configured type for primary and foreign keys
+    primary_key_type, foreign_key_type = primary_and_foreign_key_types
+
+    create_table :active_storage_blobs, id: primary_key_type do |t|
+      t.string   :key,          null: false
+      t.string   :filename,     null: false
+      t.string   :content_type
+      t.text     :metadata
+      t.string   :service_name, null: false
+      t.bigint   :byte_size,    null: false
+      t.string   :checksum
+
+      if connection.supports_datetime_with_precision?
+        t.datetime :created_at, precision: 6, null: false
+      else
+        t.datetime :created_at, null: false
+      end
+
+      t.index [ :key ], unique: true
+    end
+
+    create_table :active_storage_attachments, id: primary_key_type do |t|
+      t.string     :name,     null: false
+      t.references :record,   null: false, polymorphic: true, index: false, type: foreign_key_type
+      t.references :blob,     null: false, type: foreign_key_type
+
+      if connection.supports_datetime_with_precision?
+        t.datetime :created_at, precision: 6, null: false
+      else
+        t.datetime :created_at, null: false
+      end
+
+      t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
+      t.foreign_key :active_storage_blobs, column: :blob_id
+    end
+
+    create_table :active_storage_variant_records, id: primary_key_type do |t|
+      t.belongs_to :blob, null: false, index: false, type: foreign_key_type
+      t.string :variation_digest, null: false
+
+      t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
+      t.foreign_key :active_storage_blobs, column: :blob_id
+    end
+  end
+
+  private
+    def primary_and_foreign_key_types
+      config = Rails.configuration.generators
+      setting = config.options[config.orm][:primary_key_type]
+      primary_key_type = setting || :primary_key
+      foreign_key_type = setting || :bigint
+      [ primary_key_type, foreign_key_type ]
+    end
+end
diff --git a/db/migrate/20250117001421_add_storage_to_audios.rb b/db/migrate/20250117001421_add_storage_to_audios.rb
new file mode 100644 (file)
index 0000000..8e5d7ef
--- /dev/null
@@ -0,0 +1,16 @@
+class AddStorageToAudios < ActiveRecord::Migration[8.0]
+  def change
+    # Remove the old audio_url column
+    remove_column :audios, :audio_url if column_exists?(:audios, :audio_url)
+    
+    # Add description if it doesn't exist
+    unless column_exists?(:audios, :description)
+      add_column :audios, :description, :text
+    end
+
+    # Ensure not null constraints
+    change_column_null :audios, :title, false
+    change_column_null :audios, :duration_in_seconds, false
+    change_column_null :audios, :published_at, false
+  end
+end
diff --git a/db/migrate/20250117002947_update_audio_duration_constraint.rb b/db/migrate/20250117002947_update_audio_duration_constraint.rb
new file mode 100644 (file)
index 0000000..5b914dc
--- /dev/null
@@ -0,0 +1,5 @@
+class UpdateAudioDurationConstraint < ActiveRecord::Migration[8.0]
+  def change
+    change_column_null :audios, :duration_in_seconds, true
+  end
+end
index 582226db83bf41d53436dfd4ea509f7c1ea9e28d..77e07957e2e7396416f995d0679c82dd384a7540 100644 (file)
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[8.0].define(version: 2025_01_13_020623) do
+ActiveRecord::Schema[8.0].define(version: 2025_01_17_002947) do
+  create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
+    t.string "name", null: false
+    t.string "record_type", null: false
+    t.bigint "record_id", null: false
+    t.bigint "blob_id", null: false
+    t.datetime "created_at", null: false
+    t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+    t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+  end
+
+  create_table "active_storage_blobs", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
+    t.string "key", null: false
+    t.string "filename", null: false
+    t.string "content_type"
+    t.text "metadata"
+    t.string "service_name", null: false
+    t.bigint "byte_size", null: false
+    t.string "checksum"
+    t.datetime "created_at", null: false
+    t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
+  end
+
+  create_table "active_storage_variant_records", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
+    t.bigint "blob_id", null: false
+    t.string "variation_digest", null: false
+    t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
+  end
+
   create_table "api_keys", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
     t.string "key"
     t.datetime "created_at", null: false
@@ -27,6 +55,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_13_020623) do
     t.integer "user_id"
   end
 
+  create_table "audios", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
+    t.string "title", null: false
+    t.text "description"
+    t.integer "duration_in_seconds"
+    t.datetime "published_at", null: false
+    t.bigint "post_id", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["post_id"], name: "index_audios_on_post_id"
+  end
+
   create_table "pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
     t.string "title"
     t.string "slug"
@@ -93,5 +132,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_13_020623) do
     t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
   end
 
+  add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
+  add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
   add_foreign_key "api_keys", "users"
+  add_foreign_key "audios", "posts"
 end