Compare commits

..

7 Commits
v0.2.0 ... main

Author SHA1 Message Date
77af2e4c82 Fixed small error in woodpecker CI config
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-03-18 11:46:32 -04:00
025e7c81e9 Fixed small error in woodpecker CI config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-03-18 11:45:34 -04:00
1595e150f4 Fixed small error in woodpecker CI config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-03-18 11:43:23 -04:00
47885845a3 Added woodpecker CI config
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-03-18 11:41:13 -04:00
9c91bcb6cb Added Rubocop to project; corrected style errors 2023-03-18 11:35:52 -04:00
f27154b705 Added ability to mark or 'star' a video to keep a short list of active video projects 2023-03-13 22:23:34 -04:00
85750b4de4 Added rubocop to project and started cleaning up some code style issues 2023-03-10 12:10:44 -05:00
24 changed files with 307 additions and 131 deletions

21
.rubocop.yml Normal file
View File

@ -0,0 +1,21 @@
require: rubocop-sequel
AllCops:
NewCops: enable
Layout/EmptyLinesAroundClassBody:
EnforcedStyle: 'empty_lines_except_namespace'
Layout/EmptyLinesAroundModuleBody:
EnforcedStyle: 'empty_lines_except_namespace'
Metrics/ClassLength:
Max: 150
Style/ClassVars:
Enabled: false
Style/GlobalVars:
Enabled: false
Style/MethodCallWithoutArgsParentheses:
Enabled: false
Style/MethodCallWithArgsParentheses:
Enabled: true
Style/RedundantReturn:
Enabled: false

19
.woodpecker.yml Normal file
View File

@ -0,0 +1,19 @@
pipeline:
style:
image: ruby:3.0
commands:
- 'gem install rake'
- 'bundle config set --local path "vendor/bundle"'
- 'bundle install'
- 'rake test:rubocop'
gitea_release:
image: plugins/gitea-release
settings:
api_key:
from_secret: gitea_api_key
base_url: https://git.metaunix.net
title: "${CI_COMMIT_TAG}"
when:
event: tag

11
Gemfile
View File

@ -11,7 +11,12 @@ 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?
group :development, :test do
# Use guard-rack gem to auto-reload app
gem 'guard-rack'
gem 'wdm', '>= 0.1.0' if Gem.win_platform?
# rubocop and extensions for code style
gem 'rubocop'
gem 'rubocop-sequel'
end

View File

@ -1,6 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
coderay (1.1.3)
ffi (1.15.5)
ffi (1.15.5-x64-mingw-ucrt)
@ -18,6 +19,7 @@ GEM
ffi
guard (~> 2.3)
spoon
json (2.6.3)
kramdown (2.4.0)
rexml
listen (3.8.0)
@ -34,6 +36,9 @@ GEM
nenv (~> 0.1)
shellany (~> 0.0)
pandoc-ruby (2.1.7)
parallel (1.22.1)
parser (3.2.1.1)
ast (~> 2.4.1)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
@ -42,10 +47,27 @@ GEM
rack (2.2.6.2)
rack-protection (3.0.5)
rack
rainbow (3.1.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (2.7.0)
rexml (3.2.5)
rubocop (1.48.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.27.0)
parser (>= 3.2.1.0)
rubocop-sequel (0.3.4)
rubocop (~> 1.0)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sequel (5.66.0)
shellany (0.0.1)
@ -66,6 +88,7 @@ GEM
sqlite3 (1.6.1-x86_64-linux)
thor (1.2.1)
tilt (2.1.0)
unicode-display_width (2.4.2)
PLATFORMS
x64-mingw-ucrt
@ -76,6 +99,8 @@ DEPENDENCIES
kramdown (~> 2.4)
pandoc-ruby (~> 2.1)
puma (~> 6.1)
rubocop
rubocop-sequel
sequel (~> 5.66)
sinatra (~> 3.0)
sinatra-contrib (~> 3.0)

View File

@ -23,3 +23,9 @@ namespace :server do
%x{guard}
end
end
namespace :test do
task :rubocop do
system("rubocop app/ server.rb scan.rb")
end
end

View File

@ -1,18 +1,24 @@
# frozen_string_literal: true
require 'yaml'
# Configuration loader - loads config defaults and an optional config file
class Config
DEFAULT_CONFIG = 'config/defaults.yaml'
def initialize(config_path)
@data = YAML::load_file(DEFAULT_CONFIG)
# Load the default config
@data = YAML.load_file(DEFAULT_CONFIG)
if File.exists?(config_path)
@data.merge!(YAML::load_file(config_path))
end
# End if the optional config file doesn't exist
return unless File.exist?(config_path)
# If the optional config exists, load it up!
@data.merge!(YAML.load_file(config_path))
end
def get(key, depth = 0)
def get(key)
bits = key.split('.')
value = @data

View File

@ -1,11 +1,14 @@
# frozen_string_literal: true
# ERB view helper functions
module Helpers
def nullable(value)
if (value) and (value != '')
return value
else
return 'N/a'
end
# Returns the value if it actually exists
return value if value && (value != '')
# Returns a default 'N/a' string
return 'N/a'
end
def date_format(date)

View File

@ -1,22 +1,25 @@
# frozen_string_literal: true
require 'fileutils'
# Model handles YouTube channels
class Channel < Sequel::Model
one_to_many :videos
def ensureDirectoryStructure()
def ensure_directory_structure
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
FileUtils.mkdir_p(sub_path)
end
end
def openProjects()
return self.videos_dataset.exclude(status: 'published').exclude(archived: true).all()
def open_projects
return videos_dataset.exclude(status: 'published').exclude(archived: true).all()
end
end

View File

@ -1,32 +1,35 @@
# frozen_string_literal: true
require 'fileutils'
require 'kramdown'
require 'pandoc-ruby'
# Model handles Video projects
class Video < Sequel::Model
many_to_one :channel
def ensureDirectoryStructure()
sub_dirs = ['Audio', 'B-Roll', 'Clips', 'Images', 'Export']
def ensure_directory_structure
sub_dirs = %w[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
FileUtils.mkdir_p(sub_path)
end
end
def parseScript()
return Kramdown::Document.new(@values[:script]).to_html
def parse_script
Kramdown::Document.new(@values[:script]).to_html
end
def importScript()
def import_script
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()
# save changes to the model
save_changes()
end
end

View File

@ -1,5 +1,7 @@
require_relative 'routes/index.rb'
# frozen_string_literal: true
require_relative 'routes/channel.rb'
require_relative 'routes/index'
require_relative 'routes/video.rb'
require_relative 'routes/channel'
require_relative 'routes/video'

View File

@ -1,8 +1,11 @@
# frozen_string_literal: true
class StageManager
# Controller to handle API v1 routes
class ApiV1Controller
get '/health' do
json :status => 'success'
json status: 'success'
end
get '/channels' do
@ -16,4 +19,4 @@ class StageManager
end
end
end
end

View File

@ -1,4 +1,7 @@
# frozen_string_literal: true
class StageManager
# Channel that handles channel top-level routes
class ChannelController
get '/' do
@ -6,16 +9,16 @@ class StageManager
end
get '/list' do
channels = Channel.reverse(:updated_at).all()
erb :'channel/list', :locals => {
:title => 'List of channels',
:channels => channels
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')
erb :'channel/create', locals: {
title: 'Create new channel',
base_directory: $conf.get('stgm.base_directory')
}
end
post '/create' do
@ -27,7 +30,7 @@ class StageManager
# create supporting directory structure
Dir.mkdir(channel.directory_path)
channel.ensureDirectoryStructure()
channel.ensure_directory_structure()
redirect "/channel/#{channel.id}"
end
@ -35,18 +38,18 @@ class StageManager
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
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
erb :'channel/edit', locals: {
title: "Editing: #{channel.name}",
channel: channel
}
end
post '/:channel_id/edit' do
@ -77,9 +80,9 @@ class StageManager
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]
attr_to_edit = params[:attr]
if attrToEdit == 'directory_path'
if attr_to_edit == '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])
@ -87,10 +90,10 @@ class StageManager
end
end
channel[attrToEdit.to_sym] = params[:value]
channel.save()
channel[attr_to_edit.to_sym] = params[:value]
channel.save_changes()
return "success"
return 'success'
end
end

View File

@ -1,13 +1,18 @@
# frozen_string_literal: true
class StageManager
# Controller to handle top-level pages
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
active_projects = Video.where(starred: true).reverse(:updated_at).limit($conf.get('stgm.max_stars'))
erb :index, locals: {
title: 'Dashboard',
channels: channels,
videos: videos,
active_projects: active_projects
}
end

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true
require 'fileutils'
class StageManager
# Controller to handle top-level video routes
class VideoController
get '/' do
@ -8,24 +11,21 @@ class StageManager
end
get '/list' do
videos = Video.reverse(:updated_at).all()
erb :'video/list', :locals => {
:title => 'List of videos',
:videos => videos
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
selected_channel = params[:channel].to_i() if params.key?(:channel)
channels = Channel.all()
erb :'video/create', :locals => {
:title => 'Create new video',
:channels => channels,
:selected_channel => selected_channel
erb :'video/create', locals: {
title: 'Create new video',
channels: channels,
selected_channel: selected_channel
}
end
post '/create' do
@ -48,34 +48,34 @@ class StageManager
# create supporting directory structure
Dir.mkdir(video_path)
video.ensureDirectoryStructure()
video.ensure_directory_structure()
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
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
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
erb :'video/edit', locals: {
title: "Editing: #{video.name}",
video: video,
channels: channels
}
end
post '/:video_id/edit' do
@ -110,20 +110,20 @@ class StageManager
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]
attr_to_edit = params[:attr]
# if we update the video's serial, we need to also update the directory path
if attrToEdit == 'serial'
if attr_to_edit == '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()
video[attr_to_edit.to_sym] = params[:value]
video.save_changes()
return "success"
return 'success'
end
get '/:video_id/archive' do
@ -145,8 +145,9 @@ class StageManager
archived: true
)
redirect '/video/' + video.id.to_s()
redirect "/video/#{video.id}"
end
get '/:video_id/unarchive' do
# find the video
video = Video.where(id: params[:video_id]).first()
@ -166,14 +167,14 @@ class StageManager
archived: false
)
redirect '/video/' + video.id.to_s()
redirect "/video/#{video.id}"
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
erb :'video/edit-script', locals: {
title: "Editing script: #{video.name}",
video: video
}
end
post '/:video_id/edit/script' do

View File

@ -1,6 +1,8 @@
$(document).ready(() ->
$('.channel-path').click(updateAttribute)
$('.video-active').click(updateAttribute)
$('.video-status').click(updateAttribute)
$('.video-serial').click(updateAttribute)
@ -14,6 +16,8 @@ updateAttribute = (e) ->
if newValue
if attr == 'status'
newValue = newValue.toLowerCase()
if attr == 'starred'
newValue = (String(newValue).toLowerCase() == 'true')
payload =
value: newValue
objectId = $('#' + type + '-id').val()
@ -22,6 +26,8 @@ updateAttribute = (e) ->
if data == 'success'
if attr == 'status'
newValue = capitalizeWord(newValue)
if attr == 'starred'
newValue = if newValue then 'True' else 'False'
elem.find('span').text(newValue)
)
else

View File

@ -156,9 +156,7 @@ hr{
}
}
.channel-path,
.video-status,
.video-serial{
.clickable-attr{
transition: background 230ms ease-in-out;
&:hover{

View File

@ -1,5 +1,6 @@
stgm:
base_directory: '/srv/videos'
max_stars: 5
server:
address: '127.0.0.1'

View File

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

45
scan.rb
View File

@ -1,24 +1,24 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'pathname'
require 'sequel'
require_relative 'lib/config.rb'
require_relative 'app/config'
# Load configuration file
$conf = Config.new(File.join(__dir__, 'data/defaults.yaml'))
conf = Config.new(File.join(__dir__, 'data/defaults.yaml'))
# Load the Sequel timestamps plugin
Sequel::Model.plugin :timestamps
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').sort().each { |f| require f }
unless ARGV.length == 1
abort 'You must supply a channel name!'
end
# fail if the script is being called incorrectly
abort('You must supply a channel name!') unless ARGV.length == 1
channel = Channel.where(name: ARGV[0]).first()
channel_dir = File.join(channel.directory_path, 'Main')
@ -30,7 +30,7 @@ subs.each do |d|
# parse video serial from folder name
serial_raw = dir_name[0..5].strip()
video_serial = serial_raw[1..-1]
video_serial = serial_raw.slice(0)
# parse video name from folder name
video_name = dir_name.split(' - ')[1]
@ -38,19 +38,20 @@ subs.each do |d|
# 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()
# skip to next video if the video already exists
next unless db_results.empty?()
# 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 = 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()
video.import_script()
puts "Successfully added video ##{video.serial} - #{video.name} for channel '#{channel.name}' to the database."
end
puts "Successfully added video ##{video.serial} - #{video.name} for channel '#{channel.name}' to the database."
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'logger'
require 'sequel'
require 'sqlite3'
@ -6,16 +8,24 @@ require 'sinatra/json'
require 'rack/protection'
# Load the Sequel timestamps plugin
Sequel::Model.plugin :timestamps
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
Dir.glob('./app/models/*.rb').each { |f| require f }
Dir.glob('./app/models/*.rb').sort().each { |f| require f }
# Base Sinatra app
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
def self.new(*)
self < StageManager ? super : Rack::URLMap.new(@@my_app)
end
def
self.map(url) @@my_app[url] = self
end
# Enable and configure sessions
enable :sessions
@ -25,34 +35,49 @@ class StageManager < Sinatra::Base
# Set up static file serving
enable :static
set :public_folder, __dir__ + '/public'
set :public_folder, File.join(__dir__, '/public')
# Set up our view engine
set :views, settings.root + '/views'
set :views, File.join(settings.root, '/views')
# Initialize logging
logger = Logger.new(STDOUT)
logger = Logger.new($stdout)
logger.level = Logger::INFO
# Load helper functions
require_relative 'app/helpers.rb'
require_relative 'app/helpers'
helpers Helpers
# Map controllers
## Map controllers
# Top-level routes controller
class IndexController < StageManager
map '/'
end
# Channel routes controller
class ChannelController < StageManager
map '/channel'
end
# Video routes controller
class VideoController < StageManager
map '/video'
end
# API controllers
# API v1 controller
class ApiV1Controller < StageManager
map '/api/v1'
end
end
# Load controllers
Dir.glob('./app/routes/*.rb').each { |f| require f }
Dir.glob('./app/routes/*.rb').sort().each { |f| require f }

View File

@ -16,11 +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">
<div class="channel-path clickable-attr" 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().length %></span></p>
<p>Open projects: <span><%= channel.open_projects().length %></span></p>
</div>
<div class="channel-videos">
<p>Total videos: <span><%= channel.videos.length %></span></p>

View File

@ -8,6 +8,31 @@
<div class="row">
<div class="twelve columns">
<h3>Active projects:</h3>
<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>
<% active_projects.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>
<hr>
<h3>Recently updated videos:</h3>
<table class="u-full-width">
<thead>
<tr>
@ -29,6 +54,7 @@
</tbody>
</table>
<h3>Recently updated channels:</h3>
<table class="u-full-width">
<thead>
<tr>
@ -42,7 +68,7 @@
<% channels.each do |c| %>
<tr>
<td><a href="/channel/<%= c.id %>"><%= c.name %></a></td>
<td><%= nullable(c.openProjects().length) %></td>
<td><%= nullable(c.open_projects().length) %></td>
<td><%= nullable(c.videos.length) %></td>
<td><%= date_format(c.updated_at) %></td>
</tr>

View File

@ -4,6 +4,6 @@
<hr>
<%= video.parseScript() %>
<%= video.parse_script() %>
</div>
</div>

View File

@ -15,13 +15,16 @@
<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-active clickable-attr" data-attribute="starred" data-type="video">
<p>Active: <span><%= video.starred ? 'True' : 'False' %></span></p>
</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">
<div class="video-serial clickable-attr" 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">
<div class="video-status clickable-attr" data-attribute="status" data-type="video">
<p>Status: <span><%= video.status.capitalize() %></span></p>
</div>
<div class="script-controls">
@ -38,7 +41,7 @@
<hr>
<div class="script">
<%= video.parseScript() %>
<%= video.parse_script() %>
</div>
</div>
</div>