From b52e8c0a1af5cd9469865c91032957d1e2e3ecfd Mon Sep 17 00:00:00 2001 From: Miguel Michelson Date: Wed, 7 Aug 2024 02:22:46 -0400 Subject: [PATCH] zip --- .../stylesheets/application.tailwind.css | 6 +- .../playlist_purchases_controller.rb | 107 ++++++++++++ app/controllers/playlists_controller.rb | 1 + app/controllers/purchases_controller.rb | 35 +++- app/controllers/track_purchases_controller.rb | 2 +- app/helpers/playlist_purchases_helper.rb | 2 + app/helpers/tailwind_form_builder.rb | 17 +- app/jobs/zipper_job.rb | 88 +++++++--- app/models/playlist.rb | 4 + app/models/purchased_item.rb | 25 ++- app/models/track.rb | 4 + app/views/event_purchases/show.html.erb | 64 +++---- app/views/playlist_purchases/_form.html.erb | 73 ++++++++ .../create.turbo_stream.erb | 10 ++ app/views/playlist_purchases/new.html.erb | 3 + app/views/playlist_purchases/show.html.erb | 160 ++++++++++++++++++ app/views/playlists/_playlist_item.html.erb | 2 +- app/views/playlists/show.html.erb | 6 + app/views/purchases/_download_error.erb | 3 + app/views/purchases/_download_processing.erb | 4 + app/views/purchases/_download_ready.erb | 4 + app/views/purchases/_track.erb | 22 +-- app/views/purchases/music.html.erb | 8 +- app/views/purchases/tickets.html.erb | 2 +- app/views/shared/_music_purchase.html.erb | 20 ++- app/views/track_purchases/_form.html.erb | 2 + app/views/track_purchases/show.html.erb | 67 ++++---- .../user_settings/_integrations_form.html.erb | 4 +- config/routes.rb | 13 ++ .../helpers/playlist_purchases_helper_spec.rb | 15 ++ spec/requests/playlist_purchases_spec.rb | 7 + 31 files changed, 653 insertions(+), 127 deletions(-) create mode 100644 app/controllers/playlist_purchases_controller.rb create mode 100644 app/helpers/playlist_purchases_helper.rb create mode 100644 app/views/playlist_purchases/_form.html.erb create mode 100644 app/views/playlist_purchases/create.turbo_stream.erb create mode 100644 app/views/playlist_purchases/new.html.erb create mode 100644 app/views/playlist_purchases/show.html.erb create mode 100644 app/views/purchases/_download_error.erb create mode 100644 app/views/purchases/_download_processing.erb create mode 100644 app/views/purchases/_download_ready.erb create mode 100644 spec/helpers/playlist_purchases_helper_spec.rb create mode 100644 spec/requests/playlist_purchases_spec.rb diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 0822877..95686c4 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -213,7 +213,7 @@ .select, select { - @apply block w-full rounded-md border-0 px-3 pl-3 pr-10 text-default ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-brand-600 sm:text-sm sm:leading-6; + @apply block w-full rounded-md border-0 px-3 pl-3 pr-10 text-default ring-1 ring-inset ring-subtle focus:ring-2 focus:ring-brand-600 sm:text-sm sm:leading-6; } label { @@ -261,11 +261,11 @@ oijoijoij } */ .my-react-select-container .my-react-select__control { - @apply bg-white dark:bg-neutral-700 border-2 border-neutral-300 dark:border-neutral-700 hover:border-neutral-400 dark:hover:border-neutral-500; + @apply bg-default border-2 border-subtle hover:border-muted; } .my-react-select-container .my-react-select__control--is-focused { - @apply border-neutral-500 hover:border-neutral-500 dark:border-neutral-400 dark:hover:border-neutral-400 shadow-none; + @apply border-subtle hover:border-subtle shadow-none; } .my-react-select-container .my-react-select__menu { diff --git a/app/controllers/playlist_purchases_controller.rb b/app/controllers/playlist_purchases_controller.rb new file mode 100644 index 0000000..203b1c6 --- /dev/null +++ b/app/controllers/playlist_purchases_controller.rb @@ -0,0 +1,107 @@ +class PlaylistPurchasesController < ApplicationController + before_action :authenticate_user! + + def new + @playlist = Playlist.friendly.find(params[:playlist_id]) + @payment = Payment.new + @payment.assign_attributes(initial_price: @playlist.price) + @purchase = current_user.purchases.new + end + + def create + @playlist = Playlist.friendly.find(params[:playlist_id]) + @payment = Payment.new + @payment.assign_attributes(build_params) + + customer = current_user + + price = @playlist.price + price_param = build_params[:price].to_f + if @playlist.name_your_price? && price_param && price_param > @playlist.price.to_f + price = price_param + end + + @purchase = current_user.purchases.new(purchasable: @playlist, price: price) + + @purchase.virtual_purchased = [ + VirtualPurchasedItem.new({resource: @playlist, quantity: 1}) + ] + + handle_stripe_session + end + + def handle_stripe_session + account = @playlist.user.oauth_credentials.find_by(provider: "stripe_connect") + Stripe.stripe_account = account.uid unless account.blank? + + ActiveRecord::Base.transaction do + @purchase.store_items + @purchase.save + + line_items = [{ + "quantity" => 1, + "price_data" => { + "unit_amount" => (@purchase.price * 100).to_i, + "currency" => "USD", + "product_data" => { + "name" => @playlist.title, + "description" => "#{@playlist.title} from #{@playlist.user.username}" + } + } + }] + + fee_amount = ENV.fetch('PLATFORM_EVENTS_FEE', 3).to_i + + payment_intent_data = {} + + if account + payment_intent_data = { + application_fee_amount: fee_amount + } + end + + @session = Stripe::Checkout::Session.create( + payment_method_types: ["card"], + line_items: line_items, + payment_intent_data: payment_intent_data, + customer_email: current_user.email, + mode: "payment", + success_url: success_playlist_playlist_purchase_url(@playlist, @purchase), + cancel_url: failure_playlist_playlist_purchase_url(@playlist, @purchase) + ) + + @purchase.update( + checkout_type: "stripe", + checkout_id: @session["id"] + ) + + @payment_url = @session["url"] + end + end + + def success + @playlist = Playlist.friendly.find(params[:playlist_id]) + @purchase = current_user.purchases.find(params[:id]) + + if params[:enc].present? + decoded_purchase = Purchase.find_signed(CGI.unescape(params[:enc])) + @purchase.complete_purchase! if decoded_purchase.id == @purchase.id + end + + render "show" + end + + def failure + @playlist = Playlist.friendly.find(params[:playlist_id]) + @purchase = current_user.purchases.find(params[:id]) + render "show" + end + + private + + def build_params + params.require(:payment).permit( + :include_message, :optional_message, :price + ) + end +end diff --git a/app/controllers/playlists_controller.rb b/app/controllers/playlists_controller.rb index 2018091..e4393e8 100644 --- a/app/controllers/playlists_controller.rb +++ b/app/controllers/playlists_controller.rb @@ -70,6 +70,7 @@ def playlist_params :record_label, :buy_link, :buy_link_title, :enable_label, :copyright, + :name_your_price, :attribution, :noncommercial, :non_derivative_works, :copies, track_playlists_attributes: [ :id, diff --git a/app/controllers/purchases_controller.rb b/app/controllers/purchases_controller.rb index 6726c6e..074a581 100644 --- a/app/controllers/purchases_controller.rb +++ b/app/controllers/purchases_controller.rb @@ -11,6 +11,39 @@ def tickets def music kind = (params[:tab] == "tracks") ? "Track" : "Playlist" - @collection = current_user.purchases.where(state: "paid", purchasable_type: kind).page + type = params[:type] || "paid" + @collection = current_user.purchases.where(state: type, purchasable_type: kind).page + end + + def download + @purchase = Purchase.find(params[:id]) + + if @purchase.purchasable.zip.attached? + render turbo_stream: turbo_stream.replace( + "purchase_#{@purchase.id}_download", + partial: 'purchases/download_ready', + locals: { purchase: @purchase } + ) + else + ZipperJob.perform_later(purchase_id: @purchase.id) + render turbo_stream: turbo_stream.replace( + "purchase_#{@purchase.id}_download", + partial: 'purchases/download_processing', + locals: { purchase: @purchase } + ) + end + end + + def check_zip_status + @purchase = Purchase.find(params[:id]) + if @purchase.purchasable.zip.attached? + render turbo_stream: turbo_stream.replace( + "purchase_#{@purchase.id}_download", + partial: 'purchases/download_ready', + locals: { purchase: @purchase } + ) + else + head :no_content + end end end diff --git a/app/controllers/track_purchases_controller.rb b/app/controllers/track_purchases_controller.rb index 6534478..9047923 100644 --- a/app/controllers/track_purchases_controller.rb +++ b/app/controllers/track_purchases_controller.rb @@ -56,7 +56,7 @@ def handle_stripe_session puts line_items - fee_amount = 3 + fee_amount = ENV.fetch('PLATFORM_EVENTS_FEE', 3).to_i payment_intent_data = {} diff --git a/app/helpers/playlist_purchases_helper.rb b/app/helpers/playlist_purchases_helper.rb new file mode 100644 index 0000000..f8e2fd9 --- /dev/null +++ b/app/helpers/playlist_purchases_helper.rb @@ -0,0 +1,2 @@ +module PlaylistPurchasesHelper +end diff --git a/app/helpers/tailwind_form_builder.rb b/app/helpers/tailwind_form_builder.rb index de21a4c..cefa5e3 100644 --- a/app/helpers/tailwind_form_builder.rb +++ b/app/helpers/tailwind_form_builder.rb @@ -151,7 +151,6 @@ def radio_button(method, value, options = {}) end def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") - # Conditionally build the label if options[:label] is not false info = "" unless options[:label] == false info = @template.label_tag( @@ -160,14 +159,22 @@ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") ) + field_details(method, object, options) end - # Merge in default class unless a specific class has been provided in options options.merge!(class: "self-center mt-1-- mr-2 form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out") unless options.key?(:class) - # Create the checkbox and, conditionally, its label + hint = "" + if options[:hint].present? + hint_text = options[:hint].is_a?(Proc) ? @template.capture(&options[:hint]) : options[:hint] + hint = @template.content_tag(:p, hint_text.html_safe, class: "mt-1 text-sm text-gray-500") + end + @template.tag.div(class: "flex items-center space-x-2") do - @template.check_box( + checkbox = @template.check_box( @object_name, method, objectify_options(options), checked_value, unchecked_value - ) + (info.present? ? @template.tag.div(class: "flex-col items-center") { info } : "".html_safe) + ) + label_and_hint = @template.tag.div(class: "flex-col items-center") do + (info + hint).html_safe + end + checkbox + label_and_hint end end diff --git a/app/jobs/zipper_job.rb b/app/jobs/zipper_job.rb index b084125..4b320b4 100644 --- a/app/jobs/zipper_job.rb +++ b/app/jobs/zipper_job.rb @@ -3,8 +3,16 @@ class ZipperJob < ApplicationJob queue_as :default - def perform(track_id: nil, playlist_id: nil) - if track_id + def perform(track_id: nil, playlist_id: nil, purchase_id: nil) + if purchase_id + purchase = Purchase.find(purchase_id) + resource = purchase.purchasable + if resource.is_a?(Track) + track_zip(resource) + elsif resource.is_a?(Playlist) + playlist_zip(resource) + end + elsif track_id track = Track.find_by(id: track_id) return unless track track_zip(track) @@ -52,32 +60,62 @@ def track_zip(record) Rails.logger.error "Error zipping track #{record.id}: #{e.message}" end - def playlist_zip(playlist) - zipfile_path = Rails.root.join("tmp", "#{playlist.slug}-#{playlist.id}.zip") - - Zip::File.open(zipfile_path, Zip::File::CREATE) do |zipfile| - playlist.tracks.each do |track| - next unless track.audio.attached? - audio_path = Rails.root.join("tmp", track.slug) - track.audio.download do |chunk| - File.open(audio_path, "ab") { |file| file.write(chunk) } + def playlist_zip(playlist) + zip_file = Tempfile.new(["#{playlist.slug}-#{playlist.id}", '.zip']) + + begin + Zip::File.open(zip_file.path, Zip::File::CREATE) do |zipfile| + playlist.tracks.each do |track| + next unless track.audio.attached? + + begin + track.audio.open do |file| + # Use the original filename from the blob + filename = track.audio.filename.to_s + # Add the file directly to the zip without creating a separate temp file + zipfile.get_output_stream(filename) do |os| + IO.copy_stream(file, os) + end + end + rescue StandardError => e + Rails.logger.error("Error processing track #{track.id}: #{e.message}") + end end - - zipfile.add(track.audio.filename.to_s, audio_path) - File.delete(audio_path) # Clean up the downloaded audio file end + + # Attach the zipped file to the playlist + playlist.zip.attach( + io: File.open(zip_file.path), + filename: "#{playlist.slug}.zip", + content_type: 'application/zip' + ) + + # # Broadcast the update (commented out as requested) + # Turbo::StreamsChannel.broadcast_replace_to( + # "playlist_#{playlist.id}", + # target: "playlist_#{playlist.id}_download", + # partial: 'playlists/download_ready', + # locals: { playlist: playlist } + # ) + + true # Return true if successful + rescue StandardError => e + Rails.logger.error("Failed to create zip for playlist #{playlist.id}: #{e.message}") + + # # Broadcast error message (commented out as requested) + # Turbo::StreamsChannel.broadcast_replace_to( + # "playlist_#{playlist.id}", + # target: "playlist_#{playlist.id}_download", + # partial: 'playlists/download_error', + # locals: { playlist: playlist, error: e.message } + # ) + + false # Return false if failed + ensure + zip_file.close + zip_file.unlink end - - # Here you can attach the zipped file to the playlist or whatever you want - # e.g. - playlist.zip.attach( - io: File.open(zipfile_path), - filename: "#{playlist.slug}.zip" - ) - # or store it somewhere else or let the user download it directly. - - # Clean up the generated zip file if you're not attaching it - File.delete(zipfile_path) end + end diff --git a/app/models/playlist.rb b/app/models/playlist.rb index 0876eca..06d9f52 100644 --- a/app/models/playlist.rb +++ b/app/models/playlist.rb @@ -57,6 +57,10 @@ def check_label store_accessor :metadata, :price, :decimal store_accessor :metadata, :name_your_price, :boolean + def name_your_price? + name_your_price.present? + end + def cover_url(size = nil) url = case size when :medium diff --git a/app/models/purchased_item.rb b/app/models/purchased_item.rb index c07e517..fe56b4a 100644 --- a/app/models/purchased_item.rb +++ b/app/models/purchased_item.rb @@ -15,12 +15,27 @@ class PurchasedItem < ApplicationRecord def qr url = Rails.application.routes.url_helpers.event_event_ticket_url(purchase.purchasable, signed_id) - encoded_url = ERB::Util.url_encode(url) - size = 120 - data_param = "chl=#{encoded_url}" - google_charts_url = "https://chart.googleapis.com/chart?cht=qr&chs=#{size}x#{size}&#{data_param}" + qrcode = RQRCode::QRCode.new(url) + + # Generate PNG data + png = qrcode.as_png( + bit_depth: 1, + border_modules: 4, + color_mode: ChunkyPNG::COLOR_GRAYSCALE, + color: 'black', + file: nil, + fill: 'white', + module_px_size: 6, + resize_exactly_to: false, + resize_gte_to: false, + size: 250 + ) + + # Convert to base64 for embedding in HTML + base64_image = Base64.strict_encode64(png.to_s) + "data:image/png;base64,#{base64_image}" end - + def toggle_check_in! if checked_in? update({checked_in: false, checked_in_at: nil}) diff --git a/app/models/track.rb b/app/models/track.rb index d2a191e..ef47ee4 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -113,6 +113,10 @@ def peaks=(peaks) end end + def presicion_for_currency + 0 + end + def cover_url(size = nil) url = case size when :medium diff --git a/app/views/event_purchases/show.html.erb b/app/views/event_purchases/show.html.erb index 333de14..2b178b3 100644 --- a/app/views/event_purchases/show.html.erb +++ b/app/views/event_purchases/show.html.erb @@ -1,28 +1,26 @@ -
+
-

+

Order #<%= @purchase.id %>

-
- -

Order placed -

- + View invoice @@ -33,7 +31,7 @@

Products purchased

-
+
<% @purchase.purchased_items.each do |item| %> @@ -48,11 +46,13 @@
-

+

<%= link_to item.purchased_item.title, event_event_ticket_path(@event, item.signed_id) %>

-

<%= number_to_currency item.purchased_item.price %>

-

+

+ <%= number_to_currency item.purchased_item.price %> +

+

<%= item.purchased_item.short_description %>

@@ -62,19 +62,19 @@
-
Delivery address
-
+
Delivery address
+
Floyd Miles 7363 Cynthia Pass Toronto, ON N3Y 4H8
-
Shipping updates
-
+
Shipping updates
+

f•••@example.com

1•••••••••40

- +
@@ -83,16 +83,16 @@
+
+

Products purchased

+ +
+
+ + <% @purchase.purchased_items.each do |item| %> + +
+
+
+ <%= image_tag item.purchased_item.cover_url(:small), class: "h-full w-full object-cover object-center sm:h-full sm:w-full" %> +
+ +
+

+ <%= link_to item.purchased_item.title, playlist_path(@playlist) %> +

+

<%= number_to_currency item.purchased_item.price %>

+

+ <%= item.purchased_item.zip.url %> ooo + <% # link_to "Downloads", item.purchased_item.zip.url %> +

+
+
+ + +
+ + + + <% end %> +
+ +
+
+ + + +
+ +
diff --git a/app/views/playlists/_playlist_item.html.erb b/app/views/playlists/_playlist_item.html.erb index 8783a12..4059880 100644 --- a/app/views/playlists/_playlist_item.html.erb +++ b/app/views/playlists/_playlist_item.html.erb @@ -158,7 +158,7 @@ diff --git a/app/views/playlists/show.html.erb b/app/views/playlists/show.html.erb index 35067eb..d443328 100644 --- a/app/views/playlists/show.html.erb +++ b/app/views/playlists/show.html.erb @@ -291,6 +291,12 @@ <%= auto_link sanitize(@playlist.description, auto_link: true) %>
+
+ <% if @playlist.price %> + <%= render "shared/music_purchase", resource: @playlist %> + <% end %> +
+ diff --git a/app/views/purchases/_download_error.erb b/app/views/purchases/_download_error.erb new file mode 100644 index 0000000..23aaad9 --- /dev/null +++ b/app/views/purchases/_download_error.erb @@ -0,0 +1,3 @@ +<%= turbo_frame_tag "purchase_#{purchase.id}_download" do %> + error +<% end %> \ No newline at end of file diff --git a/app/views/purchases/_download_processing.erb b/app/views/purchases/_download_processing.erb new file mode 100644 index 0000000..0b973a9 --- /dev/null +++ b/app/views/purchases/_download_processing.erb @@ -0,0 +1,4 @@ +<%= turbo_frame_tag "purchase_#{purchase.id}_download" do %> +

Your download is being prepared...

+ <%= turbo_frame_tag "purchase_#{purchase.id}_download_status", src: check_zip_status_purchase_path(purchase), refresh: { interval: 5000 } %> +<% end %> \ No newline at end of file diff --git a/app/views/purchases/_download_ready.erb b/app/views/purchases/_download_ready.erb new file mode 100644 index 0000000..8d3c34e --- /dev/null +++ b/app/views/purchases/_download_ready.erb @@ -0,0 +1,4 @@ +<%= turbo_frame_tag "purchase_#{purchase.id}_download" do %> + <%= link_to "Download ZIP", purchase.purchasable.zip.url(expires_in: 5.minute, disposition: "attachment"), class: "btn btn-primary" %> + (This links lasts 5 minutes) +<% end %> \ No newline at end of file diff --git a/app/views/purchases/_track.erb b/app/views/purchases/_track.erb index b15982a..6182d6d 100644 --- a/app/views/purchases/_track.erb +++ b/app/views/purchases/_track.erb @@ -1,8 +1,5 @@
- <%= purchase.id %> - <%= purchase.state %> -
<% purchase.purchased_items.each do |item|%> @@ -12,21 +9,26 @@ ) %>
- <%= link_to track_path(item.purchased_item.slug) do %> + <%= link_to item.purchased_item do %>

<%= item.purchased_item.title %>

- <%= gettext("Created at:") %> <%= purchase.created_at %> + <%= gettext("Created at:") %> <%= l purchase.created_at, format: :short %> <% end %>
<% end %> + <% case purchase.state %> + <% when "paid" %> + Paid + <% when "pending" %> + Pending + <% end %> + <% if purchase.is_downloadable? %> -

- <%= link_to purchase_url(purchase.signed_id), class: "group block", target: "_blank" do %> - <%= gettext("Download link") %> - <% end %> -

+ <%= turbo_frame_tag "purchase_#{purchase.id}_download" do %> + <%= button_to "Prepare Download", download_purchase_path(purchase), method: :get, class: "truncate text-sm font-medium text-brand-600" %> + <% end %> <% end %>
\ No newline at end of file diff --git a/app/views/purchases/music.html.erb b/app/views/purchases/music.html.erb index 93694c4..6225302 100644 --- a/app/views/purchases/music.html.erb +++ b/app/views/purchases/music.html.erb @@ -42,7 +42,7 @@
-
+

<%= t("purchases.my_music", section: @section) %>

@@ -51,7 +51,6 @@
+ + +
    <% @collection.each do |purchase| %> <%= render "track", purchase: purchase %> diff --git a/app/views/purchases/tickets.html.erb b/app/views/purchases/tickets.html.erb index 8bcb87d..d94bdcc 100644 --- a/app/views/purchases/tickets.html.erb +++ b/app/views/purchases/tickets.html.erb @@ -118,7 +118,7 @@