diff --git a/Gemfile b/Gemfile index 35f19b90ca20f0547204f1613091dda58278fef5..43a380a159c7075835da3bd72b1430ea344b9545 100644 --- a/Gemfile +++ b/Gemfile @@ -112,3 +112,13 @@ group :test do gem 'shoulda' gem 'shoulda-callback-matchers', '~> 1.1.1' end + +gem 'streamio-ffmpeg', '~> 1.0.0' + +# sidekiq +gem 'sidekiq' +gem 'sinatra', require: false +gem 'slim' + +# CUrl +gem 'curb', '~> 0.8.8' diff --git a/app/models/learning_object.rb b/app/models/learning_object.rb index f6a4f61b637f15ec32f1fd0c0fcba2205db1957e..b28f497a28be4e68517e93f9ae4d0448bbcbbdd8 100644 --- a/app/models/learning_object.rb +++ b/app/models/learning_object.rb @@ -38,6 +38,20 @@ class LearningObject values end + def get_bitstream_retrievelink_of name + values = @bitstreams.select { |v| v["bundleName"] == name } + unless values.empty? + return Dspace::Config.rest_url + values.first["retrieveLink"] + end + end + + def get_bitstream_filename_of name + values = @bitstreams.select { |v| v["bundleName"] == name } + unless values.empty? + return values.first["name"] + end + end + private def defaults diff --git a/app/repositories/orient_db/learning_object_repository.rb b/app/repositories/orient_db/learning_object_repository.rb index 989492fd42993fa30351368fad7562cbe88b84ef..2a3d779a4cc5ef2b34cc4118e50f58e9df5fdae0 100644 --- a/app/repositories/orient_db/learning_object_repository.rb +++ b/app/repositories/orient_db/learning_object_repository.rb @@ -15,16 +15,6 @@ module OrientDb create_edge "Likes", user.rid, learning_object.id end - # Example: - # list = repository.for(:learning_objects).all - # list.each do |learning_object| - # learning_object.inspect <LearningObject model> - # end - def all - learning_objects_hash = connection.query "SELECT FROM LearningObject" - build_objects(learning_objects_hash) || [] - end - # Usage: # learning_object = repository.for(:learning_objects).get_by_dspace_id 123 # @@ -49,6 +39,16 @@ module OrientDb edges.flatten end + def save(learning_object) + result = connection.command "INSERT INTO LearningObject CONTENT #{learning_object.to_json}" + end + + def update_property(learning_object,property,value) + if accepted_properties.include? property + connection.command "UPDATE LearningObject SET #{property}='#{value}' WHERE @rid = #{learning_object.id}" + end + end + ## # To create "index:learningobject_search" on OrientDB, use the following command: # CREATE INDEX learningobject_search @@ -95,6 +95,20 @@ module OrientDb private + def accepted_properties + ['thumbnail'] + end + + def create_edges_from_array(edge_class, id, array, unique=false) + edges = [] + array.each do |o| + unless unique && edge_exists?(edge_class, id, o.id) + edges << create_edge(edge_class, id, o.id) + end + end + edges + end + def odb_class "LearningObject" end diff --git a/app/workers/massive_likes_creator_worker.rb b/app/workers/massive_likes_creator_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..79506a6831d6571a912b2a24990054f0f60ed616 --- /dev/null +++ b/app/workers/massive_likes_creator_worker.rb @@ -0,0 +1,21 @@ +class MassiveLikesCreatorWorker + + include Sidekiq::Worker + include RepositoriesProxy + + def perform(item_id, users_ids) + + item = learning_object_repository.find item_id + + users_ids.each do |user_id| + user = User.find user_id + begin + learning_object_repository.like user, item + rescue + next + end + end + + end + +end diff --git a/app/workers/massive_views_creator_worker.rb b/app/workers/massive_views_creator_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..a5903a0a87314ecb87074abfc47cf1e90e4ab418 --- /dev/null +++ b/app/workers/massive_views_creator_worker.rb @@ -0,0 +1,21 @@ +class MassiveViewsCreatorWorker + + include Sidekiq::Worker + include RepositoriesProxy + + def perform(item_id, users_ids) + + item = learning_object_repository.find item_id + + users_ids.each do |user_id| + user = User.find user_id + begin + learning_object_repository.increment_views user, item + rescue + next + end + end + + end + +end diff --git a/app/workers/thumbnail_generator_worker.rb b/app/workers/thumbnail_generator_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..75b809f3f6141524b8a98ee833ccf87476ebb84a --- /dev/null +++ b/app/workers/thumbnail_generator_worker.rb @@ -0,0 +1,58 @@ +class ThumbnailGeneratorWorker + + include Sidekiq::Worker + include RepositoriesProxy + include Thumbnail::Creation + include Thumbnail::Formats + + def perform(learning_object_id) + + item = learning_object_repository.find(learning_object_id) + filename = item.get_bitstream_filename_of "ORIGINAL" + + size = "530x298" + + unless accepted_formats.include? file_format filename + item.thumbnail = default_thumbnail + else + begin + retrieve_link = item.get_bitstream_retrievelink_of "ORIGINAL" + file = download_bitstream(retrieve_link,filename) + rescue + puts "ERROR!!! Some error occurred during file download." + else + item.thumbnail = generate_thumbnail(file,filename,size) + delete_downloaded_bitstream(file) + end + end + + learning_object_repository.update_property(item,"thumbnail",item.thumbnail) + + end + + private + + def delete_downloaded_bitstream(file) + File.unlink(file) if File.exist?(file) + end + + def download_bitstream(url, filename) + output_dir = "/tmp" + + output_file = "#{output_dir}/#{filename}" + + c = Curl::Easy.new(url) + c.ssl_verify_peer = false + c.ssl_verify_host = false + File.open(output_file, 'wb') do |f| + # c.on_progress { |dl_total, dl_now, ul_total, ul_now| + # puts "#{dl_total}, #{dl_now}, #{ul_total}, #{ul_now}"; true + # } + c.on_body {|data| f << data; data.size } + c.perform + end + + return output_file + end + +end diff --git a/config/routes.rb b/config/routes.rb index 597cf4ff4424ca60fa87624a09dd04e2afbf84ce..dda50d5997d878742652821529a377cb0dc83b2c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do + require 'sidekiq/web' + devise_for :users namespace :auth do @@ -18,7 +20,10 @@ Rails.application.routes.draw do end resources :users + resources :highlights resources :carousels + + mount Sidekiq::Web, at: '/sidekiq' end root 'welcome#index' diff --git a/lib/dspace/client.rb b/lib/dspace/client.rb index a88bb8477206f7742e6ef4b7b675ffc8c191d031..e3dd70c904575bf94a3a9665e5051d3dc3ec4e06 100644 --- a/lib/dspace/client.rb +++ b/lib/dspace/client.rb @@ -1,2 +1,16 @@ -class OrientDb::Client +class Dspace::Client + @@client = nil + + def self.instance + if !@@client.nil? + return @@client + end + + begin + @@client = DspaceClient.new(Dspace::Config.rest_url) + rescue + raise 'Wrong orient db credentials' + end + end + end diff --git a/lib/tasks/dspace.rake b/lib/tasks/dspace.rake new file mode 100644 index 0000000000000000000000000000000000000000..f2c2770d952d93686f000382b9e5be6ec6100523 --- /dev/null +++ b/lib/tasks/dspace.rake @@ -0,0 +1,101 @@ +require 'dspace_rest_client' + +namespace :dspace do + desc "Dspace tasks" + + task import: :environment do + desc "Import DSpace items to LearningObject Repositoy" + + include RepositoriesProxy + + # Quantity of items fetched on each iteration + limit = 500 + + # Start point from where items will be fetched + offset = 0 + + loop do + + puts " --> importing items from #{offset} to #{offset+limit}" + + begin + # Get items from dspace (from offset to offset+limit) + dspace_items = get_items_from_dspace(limit,offset) + rescue + # Sleeps for a while to wait database's recovery + sleep(10.seconds) + # Goes to next iteration to retry + next + else + # Terminate loop if there are no more items to import + break if dspace_items.empty? + + # Increment offset, to get new items on next iteration + offset = offset + limit + + # Iterates through items + # Verifies if item is already on repository + # Initializes new LearningObjects + # and saves them on LearningObjects's repository + dspace_items.each do |item| + result = learning_object_repository.get_by_dspace_id item.id + if result.nil? + learning_object = initialize_learning_object item + learning_object_repository.save learning_object + end + end + + end + + end + + end + + private + + def dspace_repository + @dspace_repository ||= Dspace::Client.instance.repository.item_repository + end + + def get_items_from_dspace(limit, offset) + dspace_repository.get_all_items( + expand: ['metadata','bitstreams'], + limit: limit, offset: offset + ) + end + + def initialize_learning_object item + metadata = build_array_of(item.metadata) + bitstreams = build_array_of(item.bit_streams) + current_date = Time.new + + LearningObject.new( + :name => item.name, + :description => select_value_of(metadata, "dc.description"), + :thumbnail => '/thumbnails/default_thumbnail.jpg', + :created_at => current_date, + :last_modified => current_date, + :id_dspace => item.id, + :type => item.type, + :bitstreams => bitstreams, + :metadata => metadata + ) + end + + def build_array_of(item_content=[]) + return item_content if item_content.nil? + content_array = [] + item_content.each do |i| + content_array << i.to_h + end + return content_array + end + + def select_value_of(array, key) + descriptions = array.select { |a| a[:key] == key } + unless descriptions.empty? + descriptions.first[:value] + end + end + +end diff --git a/lib/tasks/fakeData.rake b/lib/tasks/fakeData.rake new file mode 100644 index 0000000000000000000000000000000000000000..07f90f587c7cc4d7c781eb0b039ca14ca40a26be --- /dev/null +++ b/lib/tasks/fakeData.rake @@ -0,0 +1,89 @@ +namespace :fakedata do + desc "Fake data generation tasks" + + task generate_users: :environment do + desc "Generate fake Users" + + include RepositoriesProxy + + total_new_users = 50 + + (1..total_new_users).each do |i| + user = User.new( + :name => "User#{i}", + :email => "user#{i}@inf.ufpr.br", + :password => "1234567890", + :password_confirmation => "1234567890" + ) + user.save + end + end + + task generate_data: :environment do + desc "Generate fake user Likes/Views data" + + include RepositoriesProxy + + # Quantity of items fetched on each iteration + limit = 100 + + # Start point from where items will be fetched + offset = 0 + + users = User.all + + loop do + + begin + # Get items from dspace (from offset to offset+limit) + learning_objects = learning_object_repository.all_from_offset_to_limit(limit,offset) + rescue + # Sleeps for a while to wait database's recovery + sleep(30.seconds) + # Goes to next iteration to retry + next + else + # Terminate loop if there are no more items to import + break if learning_objects.empty? + + # Increment offset, to get new items on next iteration + offset = offset + limit + + learning_objects.each do |lo| + + users_views = [] + users_likes = [] + + users.each do |user| + # Randomize "Views" + if random_check(30) + users_views << user. id + # Randomize "Likes" + if random_check(70) + users_likes << user.id + end + end + end + + MassiveViewsCreatorWorker.perform_async(lo.id, users_views) + MassiveLikesCreatorWorker.perform_async(lo.id, users_likes) + + end + + end + + end + + end + + private + + def random_check(threshold) + if rand(1..100) <= threshold + return true + else + return false + end + end + +end diff --git a/lib/tasks/thumbnail.rake b/lib/tasks/thumbnail.rake new file mode 100644 index 0000000000000000000000000000000000000000..7f7a393b1117441b0ddcd5f0a419a0cf1c241295 --- /dev/null +++ b/lib/tasks/thumbnail.rake @@ -0,0 +1,39 @@ +namespace :thumbnail do + + desc "Generate Thumbnails" + task :generate => :environment do + + include RepositoriesProxy + + # Quantity of items fetched on each iteration + limit = 500 + + # Start point from where items will be fetched + offset = 0 + + loop do + + begin + # Get items from dspace (from offset to offset+limit) + items = learning_object_repository.all_from_offset_to_limit(limit,offset) + rescue + # Sleeps for a while to wait database's recovery + sleep(30.seconds) + # Goes to next iteration to retry + next + else + # Terminate loop if there are no more items to import + break if items.empty? + + # Increment offset, to get new items on next iteration + offset = offset + limit + + items.each do |item| + ThumbnailGeneratorWorker.perform_async(item.id) + end + + end + end + end + +end diff --git a/lib/thumbnail/creation.rb b/lib/thumbnail/creation.rb new file mode 100644 index 0000000000000000000000000000000000000000..0531f65155074be2661e8ce889e30e7fd48f33f3 --- /dev/null +++ b/lib/thumbnail/creation.rb @@ -0,0 +1,68 @@ +module Thumbnail + module Creation + + include Formats + + def generate_thumbnail(input, filename, size) + unless accepted_formats.include? file_format(filename) + return default_thumbnail + else + thumbnail = thumbnail_path(filename, size) + output = "#{root_dir}#{thumbnail}" + begin + if accepted_video_formats.include? File.extname(input) + generate_video_thumbnail(input, output, size) + else + generate_image_thumbnail(input, output, size) + end + rescue + return default_thumbnail + else + return thumbnail + end + end + + end + + def default_thumbnail + @default_thumbnail ||= nil + end + + private + + def generate_video_thumbnail(input,output,size) + movie = FFMPEG::Movie.new(input) + frame = (movie.duration * 25/100).floor + movie.screenshot(output, + { seek_time: frame, resolution: size }, + preserve_aspect_ratio: :width) + end + + def generate_image_thumbnail(input,output,size) + # Read the image and resize it. The `change_geometry' method + # computes the new image geometry and yields to a block. The + # return value of the block is the return value of the method. + img = Magick::Image.read(input)[0] + img.change_geometry!(size) { |cols, rows| img.thumbnail! cols, rows } + img.write(output) + end + + def encode_hash_from(object) + Digest::SHA1.hexdigest object + end + + def thumbnail_path(filename, size) + thumbnail_name = encode_hash_from filename + thumbnail_path = "#{thumbnails_dir}/#{thumbnail_name}_#{size}.jpg" + end + + def thumbnails_dir + @thumbnails_dir ||= "/thumbnails" + end + + def root_dir + @root_dir ||= Rails.root.join('public') + end + + end +end diff --git a/lib/thumbnail/formats.rb b/lib/thumbnail/formats.rb new file mode 100644 index 0000000000000000000000000000000000000000..4df246e9bc30576c0886551952073f63750650cc --- /dev/null +++ b/lib/thumbnail/formats.rb @@ -0,0 +1,36 @@ +module Thumbnail + module Formats + + def file_format file + unless file.nil? + file_format = File.extname(file) + else + file_format = "" + end + file_format + end + + def accepted_video_formats + lower = [".mp4", ".wmv", ".3gp", ".asf", ".avi", ".flv", ".mov", ".mpg", ".mpeg", ".rmvb", ".vob", ".webm"] + upper = [".MP4", ".WMV", ".3GP", ".ASF", ".AVI", ".FLV", ".MOV", ".MPG", ".MPEG", ".RMVB", ".VOB", ".WEBM"] + return lower + upper + end + + def accepted_image_formats + lower = [".jpg", ".jpeg", ".gif", ".png", ".bmp", ".tif"] + upper = [".JPG", ".JPEG", ".GIF", ".PNG", ".BMP", ".TIF"] + return lower + upper + end + + def accepted_other_formats + lower = [".pdf", ".pps"] + upper = [".PDF", ".PPS"] + return lower + upper + end + + def accepted_formats + return accepted_video_formats + accepted_image_formats + accepted_other_formats + end + + end +end