Much base work started
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-10-31 14:36:14 +11:00
parent 4a35bf6758
commit 88a906064f
97 changed files with 5333 additions and 2774 deletions

View File

@@ -0,0 +1,21 @@
<div class="mx-auto md:w-2/3 w-full">
<% if alert = flash[:alert] %>
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
<% end %>
<h1 class="font-bold text-4xl">Update your password</h1>
<%= form_with url: password_path(params[:token]), method: :put, class: "contents" do |form| %>
<div class="my-5">
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
</div>
<div class="my-5">
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
</div>
<div class="inline">
<%= form.submit "Save", class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,17 @@
<div class="mx-auto md:w-2/3 w-full">
<% if alert = flash[:alert] %>
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
<% end %>
<h1 class="font-bold text-4xl">Forgot your password?</h1>
<%= form_with url: passwords_path, class: "contents" do |form| %>
<div class="my-5">
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
</div>
<div class="inline">
<%= form.submit "Email reset instructions", class: "w-full sm:w-auto text-center rounded-lg px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,6 @@
<p>
You can reset your password on
<%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.
</p>

View File

@@ -0,0 +1,4 @@
You can reset your password on
<%= edit_password_url(@user.password_reset_token) %>
This link will expire in <%= distance_of_time_in_words(0, @user.password_reset_token_expires_in) %>.

View File

@@ -0,0 +1,31 @@
<div class="mx-auto md:w-2/3 w-full">
<% if alert = flash[:alert] %>
<p class="py-2 px-3 bg-red-50 mb-5 text-red-500 font-medium rounded-lg inline-block" id="alert"><%= alert %></p>
<% end %>
<% if notice = flash[:notice] %>
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
<% end %>
<h1 class="font-bold text-4xl">Sign in</h1>
<%= form_with url: session_url, class: "contents" do |form| %>
<div class="my-5">
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
</div>
<div class="my-5">
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
</div>
<div class="col-span-6 sm:flex sm:items-center sm:gap-4">
<div class="inline">
<%= form.submit "Sign in", class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
</div>
<div class="mt-4 text-sm text-gray-500 sm:mt-0">
<%= link_to "Forgot password?", new_password_path, class: "text-gray-700 underline hover:no-underline" %>
</div>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">StorageLocations#create</h1>
<p>Find me in app/views/storage_locations/create.html.erb</p>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">StorageLocations#destroy</h1>
<p>Find me in app/views/storage_locations/destroy.html.erb</p>
</div>

View File

@@ -0,0 +1,81 @@
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-900">Video Library</h1>
<% if @storage_locations.any? %>
<%= link_to "New Storage Location", new_storage_location_path,
class: "bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" %>
<% end %>
</div>
<% if @storage_locations.empty? %>
<div class="text-center py-12 bg-white rounded-lg shadow">
<div class="text-gray-500 text-lg mb-4">No storage locations found</div>
<p class="text-gray-600 mb-6">
Storage locations are automatically discovered from directories mounted under <code class="bg-gray-100 px-2 py-1 rounded">/videos</code>
</p>
<div class="text-sm text-gray-500">
<p class="mb-2">Example Docker volume mounts:</p>
<code class="block bg-gray-100 p-3 rounded text-left">
/path/to/movies:/videos/movies:ro<br>
/path/to/tv_shows:/videos/tv:ro<br>
/path/to/documentaries:/videos/docs:ro
</code>
</div>
</div>
<% else %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @storage_locations.each do |storage_location| %>
<div class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
<div class="p-6">
<h2 class="text-xl font-semibold text-gray-900 mb-2">
<%= link_to storage_location.name, storage_location,
class: "hover:text-blue-600 transition-colors" %>
</h2>
<div class="text-gray-600 text-sm mb-4">
<p class="mb-1">
<span class="font-medium">Path:</span>
<code class="bg-gray-100 px-1 py-0.5 rounded text-xs"><%= storage_location.path %></code>
</p>
<p class="mb-1">
<span class="font-medium">Type:</span>
<%= storage_location.storage_type.titleize %>
</p>
<p>
<span class="font-medium">Videos:</span>
<%= storage_location.video_count %>
</p>
</div>
<% if storage_location.accessible? %>
<div class="flex items-center text-green-600 text-sm mb-4">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
Accessible
</div>
<% else %>
<div class="flex items-center text-red-600 text-sm mb-4">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
Not Accessible
</div>
<% end %>
<div class="flex space-x-2">
<%= link_to "View Videos", storage_location,
class: "bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-3 rounded text-sm transition-colors" %>
<%= form_with(url: scan_storage_location_path(storage_location), method: :post,
class: "inline-flex") do |form| %>
<%= form.submit "Scan",
class: "bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-3 rounded text-sm cursor-pointer transition-colors" %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,118 @@
<div class="container mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-3xl font-bold text-gray-900"><%= @storage_location.name %></h1>
<p class="text-gray-600 mt-2">
<span class="font-medium">Path:</span>
<code class="bg-gray-100 px-2 py-1 rounded"><%= @storage_location.path %></code>
</p>
</div>
<div class="flex space-x-3">
<%= link_to "← Back to Library", storage_locations_path,
class: "bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" %>
<%= form_with(url: scan_storage_location_path(@storage_location), method: :post,
class: "inline-flex") do |form| %>
<%= form.submit "Scan for Videos",
class: "bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg cursor-pointer transition-colors" %>
<% end %>
</div>
</div>
<% if @videos.empty? %>
<div class="text-center py-12 bg-white rounded-lg shadow">
<div class="text-gray-500 text-lg mb-4">No videos found</div>
<p class="text-gray-600">
This storage location doesn't contain any video files yet. Try scanning for videos to add them to your library.
</p>
</div>
<% else %>
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">
Videos (<%= @videos.count %>)
</h2>
</div>
<div class="divide-y divide-gray-200">
<% @videos.each do |video| %>
<div class="p-6 hover:bg-gray-50 transition-colors">
<div class="flex items-start space-x-4">
<!-- Thumbnail placeholder -->
<div class="flex-shrink-0">
<% if video.video_assets.where(asset_type: 'thumbnail').any? %>
<%= image_tag video.video_assets.where(asset_type: 'thumbnail').first.file,
class: "w-24 h-16 object-cover rounded", alt: video.display_title %>
<% else %>
<div class="w-24 h-16 bg-gray-200 rounded flex items-center justify-center">
<svg class="w-8 h-8 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z"/>
</svg>
</div>
<% end %>
</div>
<!-- Video info -->
<div class="flex-1 min-w-0">
<h3 class="text-lg font-medium text-gray-900 mb-1">
<%= link_to video.display_title, video,
class: "hover:text-blue-600 transition-colors" %>
</h3>
<div class="flex flex-wrap gap-4 text-sm text-gray-600 mb-2">
<span>
<span class="font-medium">Duration:</span>
<%= video.format_duration %>
</span>
<span>
<span class="font-medium">Resolution:</span>
<%= video.resolution_label %>
</span>
<span>
<span class="font-medium">Size:</span>
<%= number_to_human_size(video.video_metadata['file_size']) rescue "Unknown" %>
</span>
</div>
<div class="flex items-center space-x-3 text-sm">
<% if video.web_compatible? %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Web Compatible
</span>
<% else %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Needs Transcoding
</span>
<% end %>
<% if video.processed? %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Processed
</span>
<% else %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Processing
</span>
<% end %>
<% if video.work&.title && video.work.title != video.display_title %>
<span class="text-gray-500">
Part of: <%= link_to video.work.title, video.work,
class: "hover:text-blue-600 transition-colors" %>
</span>
<% end %>
</div>
</div>
<!-- Actions -->
<div class="flex-shrink-0">
<%= link_to "Watch", video,
class: "bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition-colors" %>
</div>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,76 @@
<% content_for :title, "Videos" %>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900">Video Library</h1>
<div class="flex gap-2">
<% if @storage_locations.any? %>
<select class="rounded-md border-gray-300 border px-3 py-2 text-sm" id="storage-filter">
<option value="">All Sources</option>
<% @storage_locations.each do |location| %>
<option value="<%= location.id %>"><%= location.display_name %></option>
<% end %>
</select>
<% end %>
<%= link_to "New Storage Location", new_admin_storage_location_path, class: "bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium" %>
</div>
</div>
<% if @videos.any? %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<% @videos.each do |video| %>
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
<div class="aspect-video bg-gray-200 relative">
<%# Placeholder for thumbnails - Phase 1C will add actual thumbnails %>
<div class="absolute inset-0 flex items-center justify-center">
<svg class="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0120 8.618m6.418 2.276L11 14.914M4.418 4.418a2 2 0 00-2.828 0l-4.418 4.418a2 2 0 002.828 0l4.418-4.418a2 2 0 012.828 0l4.418 4.418a2 2 0 012.828 0l4.418-4.418z" />
</svg>
</div>
<%# Badge for source type %>
<div class="absolute top-2 left-2 bg-gray-800 text-white text-xs px-2 py-1 rounded">
<%= video.storage_location.name %>
</div>
</div>
<div class="p-4">
<h3 class="font-semibold text-gray-900 truncate mb-2">
<%= link_to video.display_title, video_path(video), class: "hover:text-blue-600" %>
</h3>
<div class="text-sm text-gray-500 space-y-1">
<div>Duration: <%= video.formatted_duration %></div>
<div>Size: <%= video.formatted_file_size %></div>
<% if video.resolution_label.present? %>
<div>Resolution: <%= video.resolution_label %></div>
<% end %>
</div>
<div class="mt-3 flex justify-between items-center">
<div class="text-xs text-gray-400">
<% if video.processing_errors.present? %>
<span class="text-red-500">Failed</span>
<% elsif video.processed? %>
<span class="text-green-500">Processed</span>
<% else %>
<span class="text-yellow-500">Processing</span>
<% end %>
</div>
<%= link_to "Watch", video_path(video), class: "bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded text-xs" %>
</div>
</div>
</div>
<% end %>
</div>
<!-- Pagination with Pagy -->
<%== pagy_nav(@pagy) %>
<% else %>
<div class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M9 16h6" />
</svg>
<h3 class="mt-2 text-sm font-semibold text-gray-900">No videos found</h3>
<p class="mt-1 text-sm text-gray-500">Get started by adding a storage location and scanning for videos.</p>
<%= link_to "Add Storage Location", new_admin_storage_location_path, class: "mt-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium" %>
</div>
<% end %>

View File

@@ -0,0 +1,106 @@
<% content_for :title, @video.display_title %>
<div class="max-w-4xl mx-auto">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="aspect-video bg-gray-200 relative">
<%# Placeholder for video player - Phase 1B will add Video.js %>
<div class="absolute inset-0 flex items-center justify-center">
<svg class="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 0 0-5.656 0M9 10h.01M15 5.5c0 0 0-4.95-5.39 0-7.28 0-7.28 0A4 4 0 0 1 5.5 8.78a4 4 0 0 1 0 7.28 0 7.28a4 4 0 0 1-7.28 0c0 0-4.95 4.95-5.39 0-7.28 0A4 4 0 0 1 15.5 5.5c0 0 0 0 7.28 0 7.28a4 4 0 0 0 0-7.28 0" />
</svg>
</div>
</div>
<div class="p-6">
<div class="mb-4">
<h1 class="text-2xl font-bold text-gray-900"><%= @video.display_title %></h1>
<% if @video.work.present? %>
<p class="text-gray-600"><%= link_to @video.work.display_title, work_path(@video.work), class: "hover:text-blue-600" %></p>
<% end %>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<h3 class="text-sm font-semibold text-gray-900 mb-2">Video Information</h3>
<dl class="space-y-2">
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">Duration:</dt>
<dd class="text-sm text-gray-900"><%= @video.formatted_duration %></dd>
</div>
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">File Size:</dt>
<dd class="text-sm text-gray-900"><%= @video.formatted_file_size %></dd>
</div>
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">Resolution:</dt>
<dd class="text-sm text-gray-900"><%= @video.resolution_label || "Unknown" %></dd>
</div>
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">Format:</dt>
<dd class="text-sm text-gray-900"><%= @video.format || "Unknown" %></dd>
</div>
</dl>
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900 mb-2">Storage Information</h3>
<dl class="space-y-2">
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">Storage Location:</dt>
<dd class="text-sm text-gray-900"><%= @video.storage_location.name %></dd>
</div>
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">Source Type:</dt>
<dd class="text-sm text-gray-900"><%= @video.source_type.humanize %></dd>
</div>
<div class="flex justify-between">
<dt class="text-sm font-medium text-gray-500">File Path:</dt>
<dd class="text-sm text-gray-900 truncate"><%= @video.file_path %></dd>
</div>
</dl>
</div>
</div>
<% if @video.video_metadata.present? %>
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-900 mb-2">Technical Details</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm font-medium text-gray-500">Video Codec:</span>
<span class="text-sm text-gray-900"><%= @video.video_codec || "N/A" %></span>
</div>
<div class="flex justify-between">
<span class="text-sm font-medium text-gray-500">Audio Codec:</span>
<span class="text-sm text-gray-900"><%= @video.audio_codec || "N/A" %></span>
</div>
<div class="flex justify-between">
<span class="text-sm font-medium text-gray-500">Frame Rate:</span>
<span class="text-sm text-gray-900"><%= @video.frame_rate || "N/A" %> fps</span>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-sm font-medium text-gray-500">Bit Rate:</span>
<span class="text-sm text-gray-900"><%= @video.bit_rate ? "#{(@video.bit_rate / 1000).round(1)} kb/s" : "N/A" %></span>
</div>
<div class="flex justify-between">
<span class="text-sm font-medium text-500">Dimensions:</span>
<span class="text-sm text-gray-900">
<%= @video.width || "N/A" %> × <%= @video.height || "N/A" %>
</span>
</div>
</div>
</div>
</div>
<% end %>
<div class="flex gap-3">
<%= link_to "Back to Videos", videos_path, class: "bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-md text-sm font-medium" %>
<% if @video.streamable? %>
<%= link_to "Watch Video", watch_video_path(@video), class: "bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium" %>
<% end %>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Works#index</h1>
<p>Find me in app/views/works/index.html.erb</p>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Works#show</h1>
<p>Find me in app/views/works/show.html.erb</p>
</div>