27 Commits

Author SHA1 Message Date
a48ab93b08 Added start of an API to return some basic stats of the app 2023-03-09 23:43:26 -05:00
daa41ac8e4 Enabled rack protection middleware 2023-03-09 23:28:22 -05:00
0927a00960 Added rake task to start server in a more production-ready manner using Puma; move config values to config directory 2023-03-09 23:22:18 -05:00
39ce636c0a Moved lib/ to app/ 2023-03-09 22:48:09 -05:00
f361e47e75 Migrated to a modular Sinatra application 2023-03-09 21:37:33 -05:00
46e4e5c079 Replaced the rerun gem with guard-rack; added some work to support Windows better; added a way to configure the application with config.yaml 2023-03-09 15:48:33 -05:00
39ebf6a535 Added wdm gem for Windows systems 2023-03-09 11:08:28 -05:00
0d854b026a Fixed channel open projects count 2023-03-07 11:54:20 -05:00
a9c4b29935 Consolidated the video and channel attribute edits into one javascript function 2023-03-07 11:29:10 -05:00
8905bed454 Fixed default values for attribute editing 2023-03-07 10:55:29 -05:00
0d652ffc90 Various changes 2023-03-07 10:53:18 -05:00
26a4f25631 Added pandoc-ruby gem to import .docx scripts 2023-03-07 10:01:34 -05:00
9278216b4a Updated videos table migration to relax some of the tightness on video serials 2023-03-07 09:27:40 -05:00
414cabb9f8 Added ability to archive videos 2023-03-07 00:07:29 -05:00
9b6e38a313 Added quick link to create new project from channel page 2023-03-06 23:44:51 -05:00
4735047682 Added status attribute to videos; added ability to edit video status and serial independently 2023-03-06 17:59:58 -05:00
f243452783 Added functionality to create directory structure for channels and videos 2023-03-06 15:59:09 -05:00
a2f2224b96 Added ability to delete videos 2023-03-05 10:11:17 -05:00
89ebf5c792 Added task to scan a channel's storage for videos to import 2023-03-05 10:02:25 -05:00
aea9415bbd Updated some styles 2023-03-04 21:31:42 -05:00
df89fd87c7 Updated some styles 2023-03-04 21:26:36 -05:00
2305a8a300 Added ability to view and edit video scripts 2023-03-04 11:37:53 -05:00
51ee9f00fb Added script attribute for videos 2023-03-04 11:14:54 -05:00
9ab06f0e9c Updated sorting for channel's videos on view page 2023-03-04 10:59:30 -05:00
5ed1771b18 Added channel and video edit pages; added link back to channel from video view page 2023-03-04 08:47:07 -05:00
ef1813f16e Added directories to channels and videos 2023-03-03 23:22:17 -05:00
db74e26124 Updated video view page; added recently updated videos to dashboard 2023-03-03 15:49:09 -05:00
43 changed files with 920 additions and 170 deletions

3
.gitignore vendored
View File

@ -59,6 +59,9 @@ build-iPhoneSimulator/
# Local database storage
data/stgm.db
# Local configuration
config/config.yaml
# Node modules for Grunt.js
node_modules/

View File

@ -7,6 +7,11 @@ gem 'puma', '~> 6.1'
gem 'sequel', '~> 5.66'
gem 'sqlite3', '~> 1.6'
# Use rerun gem to auto-reload app
gem 'rerun'
gem 'kramdown', '~> 2.4'
gem 'pandoc-ruby', '~> 2.1'
# Use rerun gem to auto-reload app
gem 'guard-rack'
gem 'wdm', '>= 0.1.0' if Gem.win_platform?

View File

@ -1,14 +1,42 @@
GEM
remote: https://rubygems.org/
specs:
coderay (1.1.3)
ffi (1.15.5)
ffi (1.15.5-x64-mingw-ucrt)
formatador (1.1.0)
guard (2.18.0)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.13.0)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-rack (2.2.1)
ffi
guard (~> 2.3)
spoon
kramdown (2.4.0)
rexml
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lumberjack (1.2.8)
method_source (1.0.0)
multi_json (1.15.0)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
nenv (0.3.0)
nio4r (2.5.8)
notiffany (0.1.3)
nenv (~> 0.1)
shellany (~> 0.0)
pandoc-ruby (2.1.7)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
puma (6.1.1)
nio4r (~> 2.0)
rack (2.2.6.2)
@ -17,10 +45,10 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rerun (0.14.0)
listen (~> 3.0)
rexml (3.2.5)
ruby2_keywords (0.0.5)
sequel (5.66.0)
shellany (0.0.1)
sinatra (3.0.5)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
@ -32,15 +60,22 @@ GEM
rack-protection (= 3.0.5)
sinatra (= 3.0.5)
tilt (~> 2.0)
spoon (0.0.6)
ffi
sqlite3 (1.6.1-x64-mingw-ucrt)
sqlite3 (1.6.1-x86_64-linux)
thor (1.2.1)
tilt (2.1.0)
PLATFORMS
x64-mingw-ucrt
x86_64-linux
DEPENDENCIES
guard-rack
kramdown (~> 2.4)
pandoc-ruby (~> 2.1)
puma (~> 6.1)
rerun
sequel (~> 5.66)
sinatra (~> 3.0)
sinatra-contrib (~> 3.0)

6
Guardfile Normal file
View File

@ -0,0 +1,6 @@
guard 'rack' do
watch('Gemfile.lock')
watch('config.ru')
watch('server.rb')
watch(%r{^(app)/.*})
end

View File

@ -7,14 +7,19 @@ namespace :db do
task :migrate do
%x{sequel -m 'db/migrations/' 'sqlite://data/stgm.db'}
end
task :import do
channel = ENV['channel']
puts %x{ruby scan.rb "#{channel}"}
end
end
namespace :server do
task :dev do
%x{ruby server.rb}
task :start do
system("bundle exec puma -C config/puma.rb")
end
task :reload do
%x{rerun --ignore 'assets/*' --ignore 'public/*' --no-notify 'ruby server.rb'}
%x{guard}
end
end

View File

@ -2,8 +2,14 @@ require 'yaml'
class Config
DEFAULT_CONFIG = 'config/defaults.yaml'
def initialize(config_path)
@data = YAML::load_file(config_path)
@data = YAML::load_file(DEFAULT_CONFIG)
if File.exists?(config_path)
@data.merge!(YAML::load_file(config_path))
end
end
def get(key, depth = 0)

View File

@ -1,4 +1,4 @@
helpers do
module Helpers
def nullable(value)
if (value) and (value != '')
@ -10,7 +10,7 @@ helpers do
def date_format(date)
dt = date.to_datetime
return dt.strftime('%B %d, %Y @ %I:%M:%S %p %Z')
return dt.strftime('%B %d, %Y, %I:%M:%S %p')
end
def date_format_input(date)

22
app/models/channel.rb Normal file
View File

@ -0,0 +1,22 @@
class Channel < Sequel::Model
one_to_many :videos
def ensureDirectoryStructure()
sub_dirs = ['Archive', 'Channel Documents', 'Main', 'Shorts']
sub_dirs.each do |d|
sub_path = File.join(
@values[:directory_path],
d
)
unless Dir.exist?(sub_path)
Dir.mkdir(sub_path)
end
end
end
def openProjects()
return self.videos_dataset.exclude(status: 'published').exclude(archived: true).all()
end
end

32
app/models/video.rb Normal file
View File

@ -0,0 +1,32 @@
require 'kramdown'
require 'pandoc-ruby'
class Video < Sequel::Model
many_to_one :channel
def ensureDirectoryStructure()
sub_dirs = ['Audio', 'B-Roll', 'Clips', 'Images', 'Export']
sub_dirs.each do |d|
sub_path = File.join(
@values[:directory_path],
d
)
unless Dir.exist?(sub_path)
Dir.mkdir(sub_path)
end
end
end
def parseScript()
return Kramdown::Document.new(@values[:script]).to_html
end
def importScript()
scripts = Dir.glob("#{@values[:directory_path]}/*Script.docx")
script_content = PandocRuby.convert([scripts[0].dump()], from: :docx, to: :markdown)
@values[:script] = script_content
self.save()
end
end

19
app/routes/api_v1.rb Normal file
View File

@ -0,0 +1,19 @@
class StageManager
class ApiV1Controller
get '/health' do
json :status => 'success'
end
get '/channels' do
channels = Channel.all().map!(&:to_hash)
json channels
end
get '/videos' do
videos = Video.all().map!(&:to_hash)
json videos
end
end
end

97
app/routes/channel.rb Normal file
View File

@ -0,0 +1,97 @@
class StageManager
class ChannelController
get '/' do
redirect '/channel/list'
end
get '/list' do
channels = Channel.reverse(:updated_at).all()
erb :'channel/list', :locals => {
:title => 'List of channels',
:channels => channels
}
end
get '/create' do
erb :'channel/create', :locals => {
:title => 'Create new channel',
:base_directory => $conf.get('stgm.base_directory')
}
end
post '/create' do
channel = Channel.create(
name: params[:channel_name],
directory_path: params[:channel_dir],
description: params[:channel_description]
)
# create supporting directory structure
Dir.mkdir(channel.directory_path)
channel.ensureDirectoryStructure()
redirect "/channel/#{channel.id}"
end
get '/:channel_id' do
channel = Channel.where(id: params[:channel_id]).first()
channel_videos = channel.videos_dataset.reverse(:updated_at).limit(10).all()
erb :'channel/view', :locals => {
:title => channel.name,
:channel => channel,
:channel_videos => channel_videos
}
end
get '/:channel_id/edit' do
channel = Channel.where(id: params[:channel_id]).first()
erb :'channel/edit', :locals => {
:title => "Editing: #{channel.name}",
:channel => channel
}
end
post '/:channel_id/edit' do
# get channel model and save old directory path
channel = Channel.where(id: params[:channel_id]).first()
old_path = channel.directory_path
# edit channel model
channel.update(
name: params[:channel_name],
directory_path: params[:channel_dir],
description: params[:channel_description]
)
# edit associate videos' directory paths
channel.videos.each do |v|
video_path = v.directory_path.sub(old_path, channel.directory_path)
v.update(directory_path: video_path)
end
# rename channel directory
File.rename(old_path, channel.directory_path)
# redirect user
redirect "/channel/#{channel.id}"
end
post '/:channel_id/edit/:attr' do
# find channel and temporarily save the old channel path
channel = Channel.where(id: params[:channel_id]).first()
attrToEdit = params[:attr]
if attrToEdit == 'directory_path'
File.rename(channel.directory_path, params[:value])
channel.videos.each do |v|
video_path = v.directory_path.sub(channel.directory_path, params[:value])
v.update(directory_path: video_path)
end
end
channel[attrToEdit.to_sym] = params[:value]
channel.save()
return "success"
end
end
end

15
app/routes/index.rb Normal file
View File

@ -0,0 +1,15 @@
class StageManager
class IndexController
get '/' do
channels = Channel.reverse(:updated_at).limit(10).all()
videos = Video.reverse(:updated_at).limit(10).all()
erb :index, :locals => {
:title => 'Dashboard',
:channels => channels,
:videos => videos
}
end
end
end

193
app/routes/video.rb Normal file
View File

@ -0,0 +1,193 @@
require 'fileutils'
class StageManager
class VideoController
get '/' do
redirect '/video/list'
end
get '/list' do
videos = Video.reverse(:updated_at).all()
erb :'video/list', :locals => {
:title => 'List of videos',
:videos => videos
}
end
get '/create' do
# check if there's a channel specified
selected_channel = false
if params.has_key?(:channel)
selected_channel = params[:channel].to_i()
end
channels = Channel.all()
erb :'video/create', :locals => {
:title => 'Create new video',
:channels => channels,
:selected_channel => selected_channel
}
end
post '/create' do
channel = Channel.where(id: params[:video_channel]).first()
video_serial = params[:video_serial].to_s.rjust(4, '0')
video_path = File.join(
channel.directory_path,
'Main',
"##{video_serial} - #{params[:video_name]}"
)
video = Video.create(
serial: params[:video_serial],
name: params[:video_name],
channel_id: params[:video_channel],
directory_path: video_path,
description: params[:video_description],
script: "# Introduction\n\n# Body\n\n# Conclusions"
)
# create supporting directory structure
Dir.mkdir(video_path)
video.ensureDirectoryStructure()
redirect "/video/#{video.id}"
end
get '/:video_id' do
video = Video.where(id: params[:video_id]).first()
erb :'video/view', :locals => {
:title => video.name,
:video => video
}
end
get '/:video_id/script' do
video = Video.where(id: params[:video_id]).first()
erb :'video/script', :locals => {
:title => "Script: #{video.name}",
:video => video
}
end
get '/:video_id/edit' do
video = Video.where(id: params[:video_id]).first()
channels = Channel.all()
erb :'video/edit', :locals => {
:title => "Editing: #{video.name}",
:video => video,
:channels => channels
}
end
post '/:video_id/edit' do
channel = Channel.where(id: params[:video_channel]).first()
video_serial = params[:video_serial].to_s.rjust(4, '0')
video_path = File.join(
channel.directory_path,
'Main',
"##{video_serial} - #{params[:video_name]}"
)
# find video and temporarily save the old video path
video = Video.where(id: params[:video_id]).first()
old_path = video.directory_path
# edit video attributes
video.update(
name: params[:video_name],
serial: params[:video_serial],
channel_id: params[:video_channel],
directory_path: video_path,
description: params[:video_description]
)
# rename the video project directory
File.rename(old_path, video_path)
# redirect the user
redirect "/video/#{video.id}"
end
post '/:video_id/edit/:attr' do
# find video and temporarily save the old video path
video = Video.where(id: params[:video_id]).first()
attrToEdit = params[:attr]
# if we update the video's serial, we need to also update the directory path
if attrToEdit == 'serial'
old_path = video.directory_path
new_path = video.directory_path.sub("##{video.serial}", "##{params[:value]}")
File.rename(old_path, new_path)
video[:directory_path] = new_path
end
video[attrToEdit.to_sym] = params[:value]
video.save()
return "success"
end
get '/:video_id/archive' do
# find the video
video = Video.where(id: params[:video_id]).first()
video_base = File.basename(video.directory_path)
# move project to channel's archive
archive_dir = File.join(
video.channel.directory_path,
'Archive',
video_base
)
FileUtils.mv(video.directory_path, archive_dir)
# mark the video as archived and update directory
video.update(
directory_path: archive_dir,
archived: true
)
redirect '/video/' + video.id.to_s()
end
get '/:video_id/unarchive' do
# find the video
video = Video.where(id: params[:video_id]).first()
video_base = File.basename(video.directory_path)
# move project to channel's archive
active_dir = File.join(
video.channel.directory_path,
'Main',
video_base
)
FileUtils.mv(video.directory_path, active_dir)
# mark the video as archived and update directory
video.update(
directory_path: active_dir,
archived: false
)
redirect '/video/' + video.id.to_s()
end
get '/:video_id/edit/script' do
video = Video.where(id: params[:video_id]).first()
erb :'video/edit-script', :locals => {
:title => "Editing script: #{video.name}",
:video => video
}
end
post '/:video_id/edit/script' do
# find video and temporarily save the old video path
video = Video.where(id: params[:video_id]).first()
# edit video attributes
video.update(
script: params[:video_script]
)
# redirect the user
redirect "/video/#{video.id}"
end
end
end

31
assets/coffee/wyrm.coffee Normal file
View File

@ -0,0 +1,31 @@
$(document).ready(() ->
$('.channel-path').click(updateAttribute)
$('.video-status').click(updateAttribute)
$('.video-serial').click(updateAttribute)
)
updateAttribute = (e) ->
elem = $(this)
type = elem.data('type')
attr = elem.data('attribute')
newValue = prompt('Enter new ' + type + ' ' + attr + ':', elem.find('span').text())
if newValue
if attr == 'status'
newValue = newValue.toLowerCase()
payload =
value: newValue
objectId = $('#' + type + '-id').val()
url = '/' + type + '/' + objectId + '/edit/' + attr
$.post(url, payload, (data) ->
if data == 'success'
if attr == 'status'
newValue = capitalizeWord(newValue)
elem.find('span').text(newValue)
)
else
console.log('User canceled attribute "' + attr + '" update.')
capitalizeWord = (str) ->
return str.charAt(0).toUpperCase() + str.slice(1)

View File

@ -1,11 +1,11 @@
$navbar-height: 60px;
$highlight-color: cornflowerblue;
$highlight-color-dark: darken($highlight-color, 10%);
$highlight-color: #2980b9;
$highlight-color-dark: lighten($highlight-color, 10%);
body{
padding-top: calc($navbar-height + 15px);
background: white;
background: #eee;
}
a{
@ -21,13 +21,24 @@ hr{
margin-bottom: 20px;
}
#main-wrapper{
max-width: 1120px;
padding: 15px 25px;
background: white;
border-radius: 8px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
}
#main-nav{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: $navbar-height;
background: $highlight-color;
border-bottom: 1px solid #999;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
z-index: 10;
ul{
display: inline-block;
@ -44,16 +55,20 @@ hr{
h3{
display: inline-block;
margin-top: 14px;
margin-left: 10px;
margin-left: 25px;
margin-bottom: 0;
color: black;
color: white;
font-size: 2.5rem;
}
li > a{
color: white;
font-size: 2rem;
font-weight: bold;
text-decoration: none;
&:hover{
color: #eee;
}
}
}
@ -61,8 +76,10 @@ hr{
margin-bottom: 10px;
}
#channel-header{
.channel-name{
#channel-header,
#video-header{
.channel-name,
.video-name{
margin-bottom: 5px;
}
.channel-created,
@ -75,14 +92,22 @@ hr{
font-style: italic;
}
.channel-description{
.video-path{
margin-top: 10px;
font-size: 2rem;
text-decoration: underline;
}
.channel-description,
.video-description{
margin-top: 10px;
font-size: 1.75rem;
}
#sidebar{
background: #ddd;
background: #eee;
border: 1px solid #666;
border-radius: 5px;
div:not(:last-child){
border-bottom: 1px solid #666;
@ -98,7 +123,8 @@ hr{
}
}
.actions-bar{
.actions-bar,
.script-controls{
padding: 0;
span{
@ -129,5 +155,44 @@ hr{
}
}
}
.channel-path,
.video-status,
.video-serial{
transition: background 230ms ease-in-out;
&:hover{
background: rgba(0, 0, 0, 0.1);
cursor: pointer;
}
}
}
}
#video_script{
max-width: 100%;
min-height: 250px;
}
#video-script{
.script{
h1{
font-size: 4rem;
}
h2{
font-size: 3.5rem;
}
h3{
font-size: 3rem;
}
h4{
font-size: 2.5rem;
}
h5{
font-size: 2.25rem;
}
h6{
font-size: 2rem;
}
}
}

9
config.ru Normal file
View File

@ -0,0 +1,9 @@
# Load application config
require_relative 'app/config.rb'
$conf = Config.new(File.join(__dir__, 'config/config.yaml'))
# Load Sinatra server
require_relative './server.rb'
# Run application
run StageManager

10
config/defaults.yaml Normal file
View File

@ -0,0 +1,10 @@
stgm:
base_directory: '/srv/videos'
server:
address: '127.0.0.1'
port: 4567
database:
adapter: 'sqlite'
database: 'data/stgm.db'

6
config/puma.rb Normal file
View File

@ -0,0 +1,6 @@
# Load application config
require './app/config.rb'
$conf = Config.new(File.join(__dir__, 'config/config.yaml'))
bind_address = "tcp://#{$conf.get('server.address')}:#{$conf.get('server.port')}"
bind bind_address

View File

@ -1,3 +0,0 @@
database:
adapter: 'sqlite'
database: 'data/stgm.db'

View File

@ -5,7 +5,7 @@ Sequel.migration do
primary_key :id
String :name, null: false
String :description
Integer :serial, null: false, unique: true
String :serial, null: false, default: 'wxyz'
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
end

View File

@ -0,0 +1,13 @@
Sequel.migration do
up do
add_column(:channels, :directory_path, String)
add_column(:videos, :directory_path, String)
end
down do
drop_column(:channels, :directory_path)
drop_column(:videos, :directory_path)
end
end

View File

@ -0,0 +1,11 @@
Sequel.migration do
up do
add_column(:videos, :script, String)
end
down do
drop_column(:videos, :script)
end
end

View File

@ -0,0 +1,11 @@
Sequel.migration do
up do
add_column(:videos, :status, String, default: 'backlog')
end
down do
drop_column(:videos, :status)
end
end

View File

@ -0,0 +1,11 @@
Sequel.migration do
up do
add_column(:videos, :archived, TrueClass, default: false)
end
down do
drop_column(:videos, :archived)
end
end

View File

@ -1,9 +0,0 @@
class Channel < Sequel::Model
one_to_many :videos
def openProjects()
return 0
end
end

View File

@ -1,5 +0,0 @@
class Video < Sequel::Model
many_to_one :channel
end

View File

@ -1,37 +0,0 @@
namespace '/channel' do
get '' do
redirect '/channel/list'
end
get '/list' do
channels = Channel.reverse(:updated_at).all()
erb :'channel/list', :locals => {
:title => 'List of channels',
:channels => channels
}
end
get '/create' do
erb :'channel/create', :locals => {
:title => 'Create new channel'
}
end
post '/create' do
channel = Channel.create(
name: params[:channel_name],
description: params[:channel_description]
)
redirect "/channel/#{channel.id}"
end
get '/:channel_id' do
channel = Channel.where(id: params[:channel_id]).first()
puts "#{channel.name}"
erb :'channel/view', :locals => {
:title => channel.name,
:channel => channel
}
end
end

View File

@ -1,12 +0,0 @@
namespace '/' do
get '' do
channels = Channel.reverse(:updated_at).limit(10).all()
erb :index, :locals => {
:title => 'Dashboard',
:channels => channels
}
end
end

View File

@ -1,41 +0,0 @@
namespace '/video' do
get '' do
redirect '/video/list'
end
get '/list' do
videos = Video.reverse(:updated_at).all()
erb :'video/list', :locals => {
:title => 'List of videos',
:videos => videos
}
end
get '/create' do
channels = Channel.all()
erb :'video/create', :locals => {
:title => 'Create new video',
:channels => channels
}
end
post '/create' do
video = Video.create(
serial: params[:video_serial],
name: params[:video_name],
channel_id: params[:video_channel],
description: params[:video_description]
)
redirect "/video/#{video.id}"
end
get '/:video_id' do
video = Video.where(id: params[:video_id]).first()
puts "#{video.name}"
erb :'video/view', :locals => {
:title => video.name,
:video => video
}
end
end

56
scan.rb Executable file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env ruby
require 'pathname'
require 'sequel'
require_relative 'lib/config.rb'
# Load configuration file
$conf = Config.new(File.join(__dir__, 'data/defaults.yaml'))
# Load the Sequel timestamps plugin
Sequel::Model.plugin :timestamps
# Initialize Sequel gem for database actions
DB = Sequel.connect(adapter: $conf.get('database.adapter'), database: $conf.get('database.database'))
# Load models
require_relative 'lib/models/channel.rb'
require_relative 'lib/models/video.rb'
unless ARGV.length == 1
abort 'You must supply a channel name!'
end
channel = Channel.where(name: ARGV[0]).first()
channel_dir = File.join(channel.directory_path, 'Main')
subs = Dir["#{channel_dir}/*"]
subs.each do |d|
# get folder basename
dir_name = File.basename(d)
# parse video serial from folder name
serial_raw = dir_name[0..5].strip()
video_serial = serial_raw[1..-1]
# parse video name from folder name
video_name = dir_name.split(' - ')[1]
# check if a video by the same serial number exists for the channel
db_results = Video.where(serial: video_serial, channel_id: channel.id).all()
# add video project to DB if there's no existing project
if db_results.length == 0
video = Video.create(
serial: video_serial,
name: video_name,
channel_id: channel.id,
directory_path: d,
description: 'TODO - imported from storage.',
script: "# Introduction\n\n# Body\n\n# Conclusions"
)
video.importScript()
puts "Successfully added video ##{video.serial} - #{video.name} for channel '#{channel.name}' to the database."
end
end

View File

@ -1,32 +1,58 @@
require 'logger'
require 'sequel'
require 'sqlite3'
require 'sinatra'
require 'sinatra/namespace'
require_relative 'lib/config.rb'
set :public_folder, __dir__ + '/public'
set :views, settings.root + '/views'
# Load configuration file
conf = Config.new(File.join(__dir__, 'data/defaults.yaml'))
# Initialize logging
logger = Logger.new(STDOUT)
logger.level = Logger::INFO
require 'sinatra/base'
require 'sinatra/json'
require 'rack/protection'
# Load the Sequel timestamps plugin
Sequel::Model.plugin :timestamps
# Initialize Sequel gem for database actions
DB = Sequel.connect(adapter: conf.get('database.adapter'), database: conf.get('database.database'))
DB = Sequel.connect(adapter: $conf.get('database.adapter'), database: $conf.get('database.database'))
# Load models
require_relative 'lib/models/channel.rb'
require_relative 'lib/models/video.rb'
Dir.glob('./app/models/*.rb').each { |f| require f }
# Load helper functions
require_relative 'lib/helpers.rb'
class StageManager < Sinatra::Base
@@my_app = {}
def self.new(*) self < StageManager ? super : Rack::URLMap.new(@@my_app) end
def self.map(url) @@my_app[url] = self end
# Register route handlers
require_relative 'lib/routes.rb'
# Enable and configure sessions
enable :sessions
# Enable rack protection middleware
use Rack::Protection
# Set up static file serving
enable :static
set :public_folder, __dir__ + '/public'
# Set up our view engine
set :views, settings.root + '/views'
# Initialize logging
logger = Logger.new(STDOUT)
logger.level = Logger::INFO
# Load helper functions
require_relative 'app/helpers.rb'
helpers Helpers
# Map controllers
class IndexController < StageManager
map '/'
end
class ChannelController < StageManager
map '/channel'
end
class VideoController < StageManager
map '/video'
end
# API controllers
class ApiV1Controller < StageManager
map '/api/v1'
end
end
# Load controllers
Dir.glob('./app/routes/*.rb').each { |f| require f }

View File

@ -14,6 +14,13 @@
</div>
</div>
<div class="row">
<div class="columns twelve">
<label for="channel_dir">Channel directory:</label>
<input class="u-full-width" type="text" id="channel_dir" name="channel_dir" required value="<%= base_directory %>">
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="channel_description">Channel description:</label>

34
views/channel/edit.erb Normal file
View File

@ -0,0 +1,34 @@
<div class="row">
<div class="twelve columns">
<h1>Editing: <%= channel.name %></h1>
</div>
</div>
<div class="row">
<div class="twelve columns">
<form action="/channel/<%= channel.id %>/edit" method="POST" class="u-full-width">
<div class="row">
<div class="columns twelve">
<label for="channel_name">Channel name:</label>
<input class="u-full-width" type="text" placeholder="My new channel" id="channel_name" name="channel_name" required value="<%= channel.name %>">
</div>
</div>
<div class="row">
<div class="columns twelve">
<label for="channel_dir">Channel directory:</label>
<input class="u-full-width" type="text" id="channel_dir" name="channel_dir" required value="<%= channel.directory_path %>">
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="channel_description">Channel description:</label>
<textarea class="u-full-width" type="text" placeholder="Description of the channel" id="channel_description" name="channel_description"><%= channel.description %></textarea>
</div>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="row">
<div class="twelve columns">
<h1>Hardware Inventory List</h1>
<h1>Channel List</h1>
</div>
</div>

View File

@ -6,6 +6,8 @@
<h4 class="channel-updated">Last updated at: <%= date_format(channel.updated_at) %></h4>
<% end %>
<p><a href="/video/create?channel=<%= channel.id %>">Create video project</a></p>
<p class="channel-description"><%= channel.description %></p>
</div>
@ -14,8 +16,11 @@
<span><a href="/channel/<%= channel.id %>/edit">Edit <i class="fa-solid fa-pen-to-square"></i></a></span><span>
<a href="/channel/<%= channel.id %>/delete">Delete <i class="fa-solid fa-trash"></i></a></span>
</div>
<div class="channel-path" data-attribute="directory_path" data-type="channel">
<p><span><%= channel.directory_path %></span></p>
</div>
<div class="channel-open">
<p>Open projects: <span><%= channel.openProjects() %></span></p>
<p>Open projects: <span><%= channel.openProjects().length %></span></p>
</div>
<div class="channel-videos">
<p>Total videos: <span><%= channel.videos.length %></span></p>
@ -27,7 +32,7 @@
<div class="row">
<div class="twelve columns">
<h3>Channel videos</h3>
<h3>Recently updated channel videos:</h3>
<% if channel.videos.length > 0 %>
<table class="u-full-width">
<thead>
@ -35,14 +40,16 @@
<th>Video name</th>
<th>Video serial</th>
<th>Video description</th>
<th>Last updated</th>
</tr>
</thead>
<tbody>
<% channel.videos.each do |v| %>
<% channel_videos.each do |v| %>
<tr>
<td><a href="/video/<%= v.id %>"><%= v.name %></a></td>
<td><%= serialize(v.serial) %></td>
<td><%= v.description %></td>
<td><%= date_format(v.updated_at) %></td>
</tr>
<% end %>
</tbody>
@ -52,3 +59,5 @@
<% end %>
</div>
</div>
<input type="hidden" id="channel-id" value="<%= channel.id %>">

View File

@ -8,6 +8,27 @@
<div class="row">
<div class="twelve columns">
<table class="u-full-width">
<thead>
<tr>
<th>Video name</th>
<th>Video serial</th>
<th>Video channel</th>
<th>Last updated</th>
</tr>
</thead>
<tbody>
<% videos.each do |v| %>
<tr>
<td><a href="/video/<%= v.id %>"><%= v.name %></a></td>
<td><%= serialize(v.serial) %></td>
<td><%= v.channel.name %></td>
<td><%= date_format(v.updated_at) %></td>
</tr>
<% end %>
</tbody>
</table>
<table class="u-full-width">
<thead>
<tr>
@ -21,7 +42,7 @@
<% channels.each do |c| %>
<tr>
<td><a href="/channel/<%= c.id %>"><%= c.name %></a></td>
<td><%= nullable(c.openProjects) %></td>
<td><%= nullable(c.openProjects().length) %></td>
<td><%= nullable(c.videos.length) %></td>
<td><%= date_format(c.updated_at) %></td>
</tr>

View File

@ -24,7 +24,7 @@
<label for="video_channel">Associated channel:</label>
<select class="u-full-width" id="video_channel" name="video_channel" required>
<% channels.each do |c| %>
<option value="<%= c.id %>"><%= c.name %></option>
<option value="<%= c.id %>" <%= 'selected' if c.id == selected_channel %>><%= c.name %></option>
<% end %>
</select>
</div>

View File

@ -0,0 +1,20 @@
<div class="row">
<div class="twelve columns">
<h1>Editing script for: <%= video.name %></h1>
</div>
</div>
<div class="row">
<div class="twelve columns">
<form action="/video/<%= video.id %>/edit/script" method="POST" class="u-full-width">
<div class="row">
<div class="twelve columns">
<label for="video_script">Video script:</label>
<textarea class="u-full-width" type="text" placeholder="Description of the video" id="video_script" name="video_script" rows="30"><%= video.script %></textarea>
</div>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>

43
views/video/edit.erb Normal file
View File

@ -0,0 +1,43 @@
<div class="row">
<div class="twelve columns">
<h1>Editing: <%= video.name %></h1>
</div>
</div>
<div class="row">
<div class="twelve columns">
<form action="/video/<%= video.id %>/edit" method="POST" class="u-full-width">
<div class="row">
<div class="columns twelve">
<label for="video_name">Video name:</label>
<input class="u-full-width" type="text" placeholder="My new video" id="video_name" name="video_name" required value="<%= video.name %>">
</div>
</div>
<div class="row">
<div class="columns three">
<label for="video_serial">Video serial:</label>
<input class="u-full-width" type="number" placeholder="0001" id="video_serial" name="video_serial" required value="<%= video.serial %>">
</div>
<div class="columns nine">
<label for="video_channel">Associated channel:</label>
<select class="u-full-width" id="video_channel" name="video_channel" required>
<% channels.each do |c| %>
<option value="<%= c.id %>"<% if c.id == video.channel_id %> selected<% end %>><%= c.name %></option>
<% end %>
</select>
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="video_description">Video description:</label>
<textarea class="u-full-width" type="text" placeholder="Description of the video" id="video_description" name="video_description"><%= video.description %></textarea>
</div>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="row">
<div class="twelve columns">
<h1>Hardware Inventory List</h1>
<h1>Video List</h1>
</div>
</div>

9
views/video/script.erb Normal file
View File

@ -0,0 +1,9 @@
<div id="video-script" class="row">
<div class="twelve columns">
<h1>Script: <%= video.name %></h1>
<hr>
<%= video.parseScript() %>
</div>
</div>

View File

@ -1,19 +1,46 @@
<div id="video-header" class="row">
<div class="twelve columns">
<h1 class="video-name"><%= video.name %></h1>
<h3 class="video-serial"><%= video.channel.name %> - <%= serialize(video.serial) %></h3>
<div class="eight columns">
<h1 class="video-name"><%= video.name %></h1><%= '<h4>(archived)</h4>' if video.archived %>
<h4 class="video-created">Item added at: <%= date_format(video.created_at) %></h4>
<% if video.updated_at %>
<h4 class="video-updated">Last updated at: <%= date_format(video.updated_at) %></h4>
<% end %>
</div>
</div>
<h4 class="video-path"><%= video.directory_path %></h4>
<div class="row">
<div class="twelve columns">
<p class="inventory-actions">
<a href="/video/<%= video.id %>/edit"><i class="fa-solid fa-pen-to-square"></i></a>
<a href="/video/<%= video.id %>/delete"><i class="fa-solid fa-trash"></i></a>
</p>
<p class="video-description"><%= video.description %></p>
</div>
<div id="sidebar" class="four columns">
<div class="actions-bar">
<span><a href="/video/<%= video.id %>/edit">Edit <i class="fa-solid fa-pen-to-square"></i></a></span><% unless video.archived %><span><a href="/video/<%= video.id %>/archive">Archive <i class="fa-solid fa-box-archive"></i></a></span><% else %><span><a href="/video/<%= video.id %>/unarchive">Unarchive <i class="fa-solid fa-box-archive"></i></a></span>
<% end %>
</div>
<div class="video-channel">
<p>Channel: <a href="/channel/<%= video.channel.id %>"><span><%= video.channel.name %></span></a></p>
</div>
<div class="video-serial" data-attribute="serial" data-type="video">
<p>Serial: <span><%= serialize(video.serial) %></span></p>
</div>
<div class="video-status" data-attribute="status" data-type="video">
<p>Status: <span><%= video.status.capitalize() %></span></p>
</div>
<div class="script-controls">
<span><a href="/video/<%= video.id %>/script">View script <i class="fa-solid fa-scroll"></i></a></span><span>
<a href="/video/<%= video.id %>/edit/script">Edit script <i class="fa-solid fa-pen-to-square"></i></a></span>
</div>
</div>
</div>
<div id="video-script" class="row">
<div class="twelve columns">
<h2>Video script</h2>
<hr>
<div class="script">
<%= video.parseScript() %>
</div>
</div>
</div>
<input type="hidden" id="video-id" value="<%= video.id %>">