diff --git a/modules/videos/controllers/admin_videos.php b/modules/videos/controllers/admin_videos.php new file mode 100644 index 00000000..5dd8abb9 --- /dev/null +++ b/modules/videos/controllers/admin_videos.php @@ -0,0 +1,96 @@ +page_title = t("Add videos from server"); + $view->content = new View("admin_videos.html"); + $view->content->form = $this->_get_admin_form(); + $paths = unserialize(module::get_var("videos", "authorized_paths", "a:0:{}")); + $view->content->paths = array_keys($paths); + + print $view; + } + + public function add_path() { + access::verify_csrf(); + + $form = $this->_get_admin_form(); + $paths = unserialize(module::get_var("videos", "authorized_paths", "a:0:{}")); + if ($form->validate()) { + if (is_link($form->add_path->path->value)) { + $form->add_path->path->add_error("is_symlink", 1); + } else if (!is_readable($form->add_path->path->value)) { + $form->add_path->path->add_error("not_readable", 1); + } else { + $path = $form->add_path->path->value; + $paths[$path] = 1; + module::set_var("videos", "authorized_paths", serialize($paths)); + message::success(t("Added path %path", array("path" => $path))); + videos::check_config($paths); + url::redirect("admin/videos"); + } + } + + $view = new Admin_View("admin.html"); + $view->content = new View("admin_videos.html"); + $view->content->form = $form; + $view->content->paths = array_keys($paths); + print $view; + } + + public function remove_path() { + access::verify_csrf(); + + $path = Input::instance()->get("path"); + $paths = unserialize(module::get_var("videos", "authorized_paths")); + if (isset($paths[$path])) { + unset($paths[$path]); + message::success(t("Removed path %path", array("path" => $path))); + module::set_var("videos", "authorized_paths", serialize($paths)); + videos::check_config($paths); + } + url::redirect("admin/videos"); + } + + public function autocomplete() { + $directories = array(); + $path_prefix = Input::instance()->get("q"); + foreach (glob("{$path_prefix}*") as $file) { + if (is_dir($file) && !is_link($file)) { + $directories[] = $file; + } + } + + print implode("\n", $directories); + } + + private function _get_admin_form() { + $form = new Forge("admin/videos/add_path", "", "post", + array("id" => "g-server-add-admin-form", "class" => "g-short-form")); + $add_path = $form->group("add_path"); + $add_path->input("path")->label(t("Path"))->rules("required")->id("g-path") + ->error_messages("not_readable", t("This directory is not readable by the webserver")) + ->error_messages("is_symlink", t("Symbolic links are not allowed")); + $add_path->submit("add")->value(t("Add Path")); + + return $form; + } +} diff --git a/modules/videos/controllers/videos.php b/modules/videos/controllers/videos.php new file mode 100644 index 00000000..00ec0691 --- /dev/null +++ b/modules/videos/controllers/videos.php @@ -0,0 +1,298 @@ +item = $item; + $view->tree = new View("videos_tree.html"); + $view->tree->files = $files; + $view->tree->parents = array(); + print $view; + } + + public function children() { + $path = Input::instance()->get("path"); + + $tree = new View("videos_tree.html"); + $tree->files = array(); + $tree->parents = array(); + + // Make a tree with the parents back up to the authorized path, and all the children under the + // current path. + if (videos::is_valid_path($path)) { + $tree->parents[] = $path; + while (videos::is_valid_path(dirname($tree->parents[0]))) { + array_unshift($tree->parents, dirname($tree->parents[0])); + } + + $glob_path = str_replace(array("{", "}", "[", "]"), array("\{", "\}", "\[", "\]"), $path); + foreach (glob("$glob_path/*") as $file) { + if (!is_readable($file)) { + continue; + } + if (!is_dir($file)) { + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + //if (!in_array($ext, array("gif", "jpeg", "jpg", "png", "flv", "mp4", "m4v"))) { + if (!in_array($ext, unserialize(module::get_var("videos", "allowed_extensions")))) { + continue; + } + } + + $tree->files[] = $file; + } + } else { + // Missing or invalid path; print out the list of authorized path + $paths = unserialize(module::get_var("videos", "authorized_paths")); + foreach (array_keys($paths) as $path) { + $tree->files[] = $path; + } + } + print $tree; + } + + /** + * Begin the task of adding files. + */ + public function start() { + access::verify_csrf(); + $item = ORM::factory("item", Input::instance()->get("item_id")); + + foreach (Input::instance()->post("paths") as $path) { + if (videos::is_valid_path($path)) { + $paths[] = array($path, null); + } + } + + $task_def = Task_Definition::factory() + ->callback("Videos_Controller::add") + ->description(t("Add videos from the local server")) + ->name(t("Add from server")); + $task = task::create($task_def, array("item_id" => $item->id, "queue" => $paths)); + + json::reply( + array("result" => "started", + "status" => (string)$task->status, + "url" => url::site("videos/run/$task->id?csrf=" . access::csrf_token()))); + } + + /** + * Run the task of adding files + */ + function run($task_id) { + access::verify_csrf(); + + $task = ORM::factory("task", $task_id); + if (!$task->loaded() || $task->owner_id != identity::active_user()->id) { + access::forbidden(); + } + + $task = task::run($task_id); + // Prevent the JavaScript code from breaking by forcing a period as + // decimal separator for all locales with sprintf("%F", $value). + json::reply(array("done" => (bool)$task->done, + "status" => (string)$task->status, + "percent_complete" => sprintf("%F", $task->percent_complete))); + } + + /** + * This is the task code that adds photos and albums. It first examines all the target files + * and creates a set of Server_Add_File_Models, then runs through the list of models and adds + * them one at a time. + */ + static function add($task) { + $mode = $task->get("mode", "init"); + $start = microtime(true); + + switch ($mode) { + case "init": + $task->set("mode", "build-file-list"); + $task->percent_complete = 0; + $task->status = t("Starting up"); + batch::start(); + break; + + case "build-file-list": // 0% to 10% + // We can't fit an arbitrary number of paths in a task, so store them in a separate table. + // Don't use an iterator here because we can't get enough control over it when we're dealing + // with a deep hierarchy and we don't want to go over our time quota. The queue is in the + // form [path, parent_id] where the parent_id refers to another Server_Add_File_Model. We + // have this extra level of abstraction because we don't know its Item_Model id yet. + $queue = $task->get("queue"); + $paths = unserialize(module::get_var("videos", "authorized_paths")); + + while ($queue && microtime(true) - $start < 0.5) { + list($file, $parent_entry_id) = array_shift($queue); + // Ignore the staging directories as directories to be imported. + if (empty($paths[$file])) { + $entry = ORM::factory("videos_file"); + $entry->task_id = $task->id; + $entry->file = $file; + $entry->parent_id = $parent_entry_id; + $entry->save(); + $entry_id = $entry->id; + } else { + $entry_id = null; + } + + $file = preg_quote($file); + foreach (glob("$file/*") as $child) { + if (is_dir($child)) { + $queue[] = array($child, $entry_id); + } else { + $ext = strtolower(pathinfo($child, PATHINFO_EXTENSION)); + //if (in_array($ext, array("gif", "jpeg", "jpg", "png", "flv", "mp4", "m4v")) && + if (in_array($ext, unserialize(module::get_var("videos", "allowed_extensions"))) && + filesize($child) > 0) { + $child_entry = ORM::factory("videos_file"); + $child_entry->task_id = $task->id; + $child_entry->file = $child; + $child_entry->parent_id = $entry_id; + $child_entry->save(); + } + } + } + } + + // We have no idea how long this can take because we have no idea how deep the tree + // hierarchy rabbit hole goes. Leave ourselves room here for 100 iterations and don't go + // over 10% in percent_complete. + $task->set("queue", $queue); + $task->percent_complete = min($task->percent_complete + 0.1, 10); + $task->status = t2( + "Found one file", "Found %count files", + ORM::factory("videos_file")->where("task_id", "=", $task->id)->count_all()); + + if (!$queue) { + $task->set("mode", "add-files"); + $task->set( + "total_files", + ORM::factory("videos_file")->where("task_id", "=", $task->id)->count_all()); + $task->percent_complete = 10; + } + break; + + case "add-files": // 10% to 100% + $completed_files = $task->get("completed_files", 0); + $total_files = $task->get("total_files"); + + // Ordering by id ensures that we add them in the order that we created the entries, which + // will create albums first. Ignore entries which already have an Item_Model attached, + // they're done. + $entries = ORM::factory("videos_file") + ->where("task_id", "=", $task->id) + ->where("item_id", "IS", null) + ->order_by("id", "ASC") + ->limit(10) + ->find_all(); + if ($entries->count() == 0) { + // Out of entries, we're done. + $task->set("mode", "done"); + } + + $owner_id = identity::active_user()->id; + foreach ($entries as $entry) { + if (microtime(true) - $start > 0.5) { + break; + } + + // Look up the parent item for this entry. By now it should exist, but if none was + // specified, then this belongs as a child of the current item. + $parent_entry = ORM::factory("videos_file", $entry->parent_id); + if (!$parent_entry->loaded()) { + $parent = ORM::factory("item", $task->get("item_id")); + } else { + $parent = ORM::factory("item", $parent_entry->item_id); + } + + $name = basename($entry->file); + $title = item::convert_filename_to_title($name); + if (is_dir($entry->file)) { + $album = ORM::factory("item"); + $album->type = "album"; + $album->parent_id = $parent->id; + $album->name = $name; + $album->title = $title; + $album->owner_id = $owner_id; + $album->save(); + $entry->item_id = $album->id; + } else { + try { + $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + if (in_array($extension, unserialize(module::get_var("videos", "allowed_extensions")))) { + $movie = ORM::factory("item"); + $movie->type = "movie"; + $movie->parent_id = $parent->id; + $movie->set_data_file($entry->file); + $movie->name = $name; + $movie->title = $title; + $movie->owner_id = $owner_id; + $movie->save(); + $entry->item_id = $movie->id; + $items_video = ORM::factory("items_video"); + $items_video->item_id = $movie->id; + $items_video->save(); + if (file_exists($entry->file . ".flv")) { + copy($entry->file . ".flv", $movie->resize_path() . ".flv"); + } + } else { + // This should never happen, because we don't add stuff to the list that we can't + // process. But just in, case.. set this to a non-null value so that we skip this + // entry. + $entry->item_id = 0; + $task->log("Skipping unknown file type: $entry->file"); + } + } catch (Exception $e) { + // This can happen if a photo file is invalid, like a BMP masquerading as a .jpg + $entry->item_id = 0; + $task->log("Skipping invalid file: $entry->file"); + } + } + + $completed_files++; + $entry->save(); + } + $task->set("completed_files", $completed_files); + $task->status = t("Adding photos / albums (%completed of %total)", + array("completed" => $completed_files, + "total" => $total_files)); + $task->percent_complete = $total_files ? 10 + 100 * ($completed_files / $total_files) : 100; + break; + + case "done": + batch::stop(); + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + db::build() + ->delete("videos_files") + ->where("task_id", "=", $task->id) + ->execute(); + message::info(t2("Successfully added one file", + "Successfully added %count files", + $task->get("completed_files"))); + } + } +} diff --git a/modules/videos/css/videos.css b/modules/videos/css/videos.css new file mode 100644 index 00000000..36746ab5 --- /dev/null +++ b/modules/videos/css/videos.css @@ -0,0 +1,38 @@ +#g-server-add button { + margin-bottom: .5em; +} + +#g-server-add-tree { + cursor: pointer; + padding-left: 4px; + width: 95%; +} + +#g-server-add-tree li { + padding: 0; + float: none; +} + +#g-server-add-tree span.selected { + background: #ddd; +} + +#g-server-add-tree { + border: 1px solid #ccc; + height: 20em; + overflow: auto; + margin-bottom: .5em; + padding: .5em; +} + +#g-server-add ul ul li { + padding-left: 1.2em; +} + +#g-server-add-paths li .ui-icon { + margin-top: .4em; +} + +#g-server-add-admin-form .textbox { + width: 400px; +} diff --git a/modules/videos/helpers/videos.php b/modules/videos/helpers/videos.php new file mode 100644 index 00000000..b75e4658 --- /dev/null +++ b/modules/videos/helpers/videos.php @@ -0,0 +1,49 @@ +Configure it now!", + array("url" => html::mark_clean(url::site("admin/videos")))), + "videos_configuration"); + } else { + site_status::clear("videos_configuration"); + } + } + + static function is_valid_path($path) { + if (!is_readable($path) || is_link($path)) { + return false; + } + + $authorized_paths = unserialize(module::get_var("videos", "authorized_paths")); + foreach (array_keys($authorized_paths) as $valid_path) { + if (strpos($path, $valid_path) === 0) { + return true; + } + } + + return false; + } +} diff --git a/modules/videos/helpers/videos_event.php b/modules/videos/helpers/videos_event.php new file mode 100644 index 00000000..54c7e860 --- /dev/null +++ b/modules/videos/helpers/videos_event.php @@ -0,0 +1,84 @@ +get("settings_menu") + ->append(Menu::factory("link") + ->id("videos") + ->label(t("Videos")) + ->url(url::site("admin/videos"))); + } + + static function site_menu($menu, $theme) { + $item = $theme->item(); + $paths = unserialize(module::get_var("videos", "authorized_paths")); + + if ($item && identity::active_user()->admin && $item->is_album() && !empty($paths) && + is_writable($item->is_album() ? $item->file_path() : $item->parent()->file_path())) { + $menu->get("add_menu") + ->append(Menu::factory("dialog") + ->id("videos") + ->label(t("Add videos")) + ->url(url::site("videos/browse/$item->id"))); + } + } + + static function item_before_delete($item) { + // If deleting a video, make sure the resize is deleted as well, if it exists. + if ($item->is_movie()) { + $items_video = ORM::factory("items_video") + ->where("item_id", "=", $item->id) + ->find(); + if ($items_video->loaded() && file_exists($item->resize_path() . ".flv")) { + @unlink($item->resize_path() . ".flv"); + } + } + } + + static function item_updated($old, $new) { + // When updating a video, check and see if the file name is being changed. + // If so, check for and modify any corresponding resized video + + if ($old->is_movie()) { + if ($old->file_path() != $new->file_path()) { + $items_video = ORM::factory("items_video") + ->where("item_id", "=", $old->id) + ->find(); + if ($items_video->loaded() && file_exists($old->resize_path() . ".flv")) { + @rename($old->resize_path() . ".flv", $new->resize_path() . ".flv"); + } + } + } + } + + static function item_moved($item, $old_parent) { + // When moving an video, also move the flash resize, if it exists. + + if ($item->is_movie()) { + $items_video = ORM::factory("items_video") + ->where("item_id", "=", $item->id) + ->find(); + $old_resize_path = $old_parent->resize_path() . "/" . $item->name . ".flv"; + if ($items_video->loaded() && file_exists($old_resize_path)) { + @rename($old_resize_path, $item->resize_path() . ".flv"); + } + } + } +} diff --git a/modules/videos/helpers/videos_installer.php b/modules/videos/helpers/videos_installer.php new file mode 100644 index 00000000..137df9cb --- /dev/null +++ b/modules/videos/helpers/videos_installer.php @@ -0,0 +1,52 @@ +query("CREATE TABLE {videos_files} ( + `id` int(9) NOT NULL auto_increment, + `file` varchar(255) NOT NULL, + `item_id` int(9), + `parent_id` int(9), + `task_id` int(9) NOT NULL, + PRIMARY KEY (`id`)) + DEFAULT CHARSET=utf8;"); + $db->query("CREATE TABLE {items_videos} ( + `id` int(9) NOT NULL auto_increment, + `item_id` int(9) NOT NULL, + PRIMARY KEY (`id`), + KEY (`item_id`, `id`)) + DEFAULT CHARSET=utf8;"); + module::set_var("videos", "allowed_extensions", serialize(array("avi", "mpg", "mpeg", "mov", "wmv", "asf", "mts"))); + module::set_version("videos", 1); + videos::check_config(); + } + + static function deactivate() { + site_status::clear("videos_configuration"); + } + + static function uninstall() { + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {videos_files};"); + $db->query("DROP TABLE IF EXISTS {items_videos};"); + module::delete("videos"); + } +} diff --git a/modules/videos/helpers/videos_theme.php b/modules/videos/helpers/videos_theme.php new file mode 100644 index 00000000..9e994591 --- /dev/null +++ b/modules/videos/helpers/videos_theme.php @@ -0,0 +1,55 @@ +admin) { + $theme->css("videos.css"); + $theme->script("videos.js"); + } + + $item = $theme->item(); + if ($item && $item->is_movie()) { + $items_video = ORM::factory("items_video") + ->where("item_id", "=", $item->id) + ->find(); + if ($items_video->loaded()) { + $view = new View("videos_display_js.html"); + //$view->embed_code = addslashes($embedded_video->embed_code); + return $view; + } + } + } + + static function admin_head($theme) { + $head = array(); + if (strpos(Router::$current_uri, "admin/videos") !== false) { + $theme->css("videos.css"); + $theme->css("jquery.autocomplete.css"); + $base = url::site("__ARGS__"); + $csrf = access::csrf_token(); + $head[] = ""; + + $theme->script("jquery.autocomplete.js"); + $theme->script("admin_videos.js"); + } + + return implode("\n", $head); + } +} \ No newline at end of file diff --git a/modules/videos/js/admin_videos.js b/modules/videos/js/admin_videos.js new file mode 100644 index 00000000..9bb61ed1 --- /dev/null +++ b/modules/videos/js/admin_videos.js @@ -0,0 +1,8 @@ +/** + * Set up autocomplete on the server path list + * + */ +$("document").ready(function() { + $("#g-path").autocomplete( + base_url.replace("__ARGS__", "admin/videos/autocomplete"), {max: 256}); +}); diff --git a/modules/videos/js/videos.js b/modules/videos/js/videos.js new file mode 100644 index 00000000..02dda4c0 --- /dev/null +++ b/modules/videos/js/videos.js @@ -0,0 +1,125 @@ +(function($) { + $.widget("ui.gallery_server_add", { + _init: function() { + var self = this; + $("#g-server-add-add-button", this.element).click(function(event) { + event.preventDefault(); + $(".g-progress-bar", this.element). + progressbar(). + progressbar("value", 0); + $("#g-server-add-progress", this.element).slideDown("fast", function() { self.start_add(); }); + }); + $("#g-server-add-pause-button", this.element).click(function(event) { + self.pause = true; + $("#g-server-add-pause-button", this.element).hide(); + $("#g-server-add-continue-button", this.element).show(); + }); + $("#g-server-add-continue-button", this.element).click(function(event) { + self.pause = false; + $("#g-server-add-pause-button", this.element).show(); + $("#g-server-add-continue-button", this.element).hide(); + self.run_add(); + }); + $("#g-server-add-close-button", this.element).click(function(event) { + $("#g-dialog").dialog("close"); + window.location.reload(); + }); + $("#g-server-add-tree span.g-directory", this.element).dblclick(function(event) { + self.open_dir(event); + }); + $("#g-server-add-tree span.g-file, #g-server-add-tree span.g-directory", this.element).click(function(event) { + self.select_file(event); + }); + $("#g-server-add-tree span.g-directory", this.element).dblclick(function(event) { + self.open_dir(event); + }); + $("#g-dialog").bind("dialogclose", function(event, ui) { + window.location.reload(); + }); + }, + + taskURL: null, + pause: false, + + start_add: function() { + var self = this; + var paths = []; + $.each($("span.selected", self.element), function () { + paths.push($(this).attr("ref")); + }); + + $("#g-server-add-add-button", this.element).hide(); + $("#g-server-add-pause-button", this.element).show(); + + $.ajax({ + url: START_URL, + type: "POST", + async: false, + data: { "paths[]": paths }, + dataType: "json", + success: function(data, textStatus) { + $("#g-status").html(data.status); + $(".g-progress-bar", self.element).progressbar("value", data.percent_complete); + self.taskURL = data.url; + setTimeout(function() { self.run_add(); }, 25); + } + }); + return false; + }, + + run_add: function () { + var self = this; + $.ajax({ + url: self.taskURL, + async: false, + dataType: "json", + success: function(data, textStatus) { + $("#g-status").html(data.status); + $(".g-progress-bar", self.element).progressbar("value", data.percent_complete); + if (data.done) { + $("#g-server-add-progress", this.element).slideUp(); + $("#g-server-add-add-button", this.element).show(); + $("#g-server-add-pause-button", this.element).hide(); + $("#g-server-add-continue-button", this.element).hide(); + } else { + if (!self.pause) { + setTimeout(function() { self.run_add(); }, 25); + } + } + } + }); + }, + + /** + * Load a new directory + */ + open_dir: function(event) { + var self = this; + var path = $(event.target).attr("ref"); + $.ajax({ + url: GET_CHILDREN_URL.replace("__PATH__", path), + success: function(data, textStatus) { + $("#g-server-add-tree", self.element).html(data); + $("#g-server-add-tree span.g-directory", self.element).dblclick(function(event) { + self.open_dir(event); + }); + $("#g-server-add-tree span.g-file, #g-server-add-tree span.g-directory", this.element).click(function(event) { + self.select_file(event); + }); + } + }); + }, + + /** + * Manage file selection state. + */ + select_file: function (event) { + $(event.target).toggleClass("selected"); + if ($("#g-server-add span.selected").length) { + $("#g-server-add-add-button").enable(true).removeClass("ui-state-disabled"); + } else { + $("#g-server-add-add-button").enable(false).addClass("ui-state-disabled"); + } + } + }); +})(jQuery); diff --git a/modules/videos/models/items_video.php b/modules/videos/models/items_video.php new file mode 100644 index 00000000..67ee29e0 --- /dev/null +++ b/modules/videos/models/items_video.php @@ -0,0 +1,22 @@ + +
+

+
+ +

+ +
+
diff --git a/modules/videos/views/movieplayer.html.php b/modules/videos/views/movieplayer.html.php new file mode 100644 index 00000000..b960e1d7 --- /dev/null +++ b/modules/videos/views/movieplayer.html.php @@ -0,0 +1,33 @@ + +where("item_id", "=", $item->id) + ->find(); + if ($items_video->loaded() && file_exists($item->resize_path() . ".flv")) { + print html::anchor(str_replace("?m=", ".flv?m=", $item->resize_url(true)), "", $attrs); + } else { + print html::anchor($item->file_url(true), "", $attrs); + } +?> + + diff --git a/modules/videos/views/videos_display_js.html.php b/modules/videos/views/videos_display_js.html.php new file mode 100644 index 00000000..6273f27a --- /dev/null +++ b/modules/videos/views/videos_display_js.html.php @@ -0,0 +1,28 @@ + + +resize_path() . ".flv")) { ?> + + diff --git a/modules/videos/views/videos_tree.html.php b/modules/videos/views/videos_tree.html.php new file mode 100644 index 00000000..91354329 --- /dev/null +++ b/modules/videos/views/videos_tree.html.php @@ -0,0 +1,37 @@ + +
  • + + + + + +
  • diff --git a/modules/videos/views/videos_tree_dialog.html.php b/modules/videos/views/videos_tree_dialog.html.php new file mode 100644 index 00000000..a235ffbf --- /dev/null +++ b/modules/videos/views/videos_tree_dialog.html.php @@ -0,0 +1,52 @@ + + + +
    +

    html::purify($item->title))) ?>

    + +

    + + + + + + + + + + + + + + + + +