From 6d231766a791def548a2c3d7d7c1638a039d7e86 Mon Sep 17 00:00:00 2001 From: javallone Date: Tue, 24 Jan 2012 20:50:07 -0500 Subject: [PATCH] Initial commit --- Gemfile | 10 ++ Gemfile.lock | 88 +++++++++++ README | 3 + config.ru | 16 ++ mplayer/control.rb | 143 +++++++++++++++++ mplayer/mplayer.rb | 2 + mplayer/property.rb | 82 ++++++++++ sideshow/app.rb | 247 ++++++++++++++++++++++++++++++ sideshow/cache.rb | 53 +++++++ sideshow/controller.rb | 88 +++++++++++ sideshow/model.rb | 71 +++++++++ sideshow/sideshow.rb | 5 + sideshow/util.rb | 122 +++++++++++++++ sideshow/views/add.erb | 19 +++ sideshow/views/dialog_layout.erb | 16 ++ sideshow/views/index.erb | 13 ++ sideshow/views/layout.erb | 37 +++++ sideshow/views/movie_list.erb | 8 + sideshow/views/program.erb | 21 +++ sideshow/views/program_list.erb | 11 ++ sideshow/views/remote.erb | 42 +++++ sideshow/views/search.erb | 8 + sideshow/views/search_results.erb | 5 + sideshow/views/settings.erb | 27 ++++ static/css/sideshow.css | 74 +++++++++ static/js/sideshow.js | 113 ++++++++++++++ 26 files changed, 1324 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README create mode 100644 config.ru create mode 100644 mplayer/control.rb create mode 100644 mplayer/mplayer.rb create mode 100644 mplayer/property.rb create mode 100755 sideshow/app.rb create mode 100644 sideshow/cache.rb create mode 100644 sideshow/controller.rb create mode 100644 sideshow/model.rb create mode 100644 sideshow/sideshow.rb create mode 100644 sideshow/util.rb create mode 100644 sideshow/views/add.erb create mode 100644 sideshow/views/dialog_layout.erb create mode 100644 sideshow/views/index.erb create mode 100644 sideshow/views/layout.erb create mode 100644 sideshow/views/movie_list.erb create mode 100644 sideshow/views/program.erb create mode 100644 sideshow/views/program_list.erb create mode 100644 sideshow/views/remote.erb create mode 100644 sideshow/views/search.erb create mode 100644 sideshow/views/search_results.erb create mode 100644 sideshow/views/settings.erb create mode 100644 static/css/sideshow.css create mode 100644 static/js/sideshow.js diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..8467dee --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +source "http://rubygems.org" +gem "rack" +gem "sinatra" + +gem "ken" +gem "data_mapper" +gem "dm-sqlite-adapter" +gem "amatch" + +gem "redis" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..810e485 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,88 @@ +GEM + remote: http://rubygems.org/ + specs: + addressable (2.2.6) + amatch (0.2.9) + tins (~> 0.3) + bcrypt-ruby (3.0.1) + data_mapper (1.2.0) + dm-aggregates (~> 1.2.0) + dm-constraints (~> 1.2.0) + dm-core (~> 1.2.0) + dm-migrations (~> 1.2.0) + dm-serializer (~> 1.2.0) + dm-timestamps (~> 1.2.0) + dm-transactions (~> 1.2.0) + dm-types (~> 1.2.0) + dm-validations (~> 1.2.0) + data_objects (0.10.7) + addressable (~> 2.1) + dm-aggregates (1.2.0) + dm-core (~> 1.2.0) + dm-constraints (1.2.0) + dm-core (~> 1.2.0) + dm-core (1.2.0) + addressable (~> 2.2.6) + dm-do-adapter (1.2.0) + data_objects (~> 0.10.6) + dm-core (~> 1.2.0) + dm-migrations (1.2.0) + dm-core (~> 1.2.0) + dm-serializer (1.2.1) + dm-core (~> 1.2.0) + fastercsv (~> 1.5.4) + json (~> 1.6.1) + json_pure (~> 1.6.1) + multi_json (~> 1.0.3) + dm-sqlite-adapter (1.2.0) + dm-do-adapter (~> 1.2.0) + do_sqlite3 (~> 0.10.6) + dm-timestamps (1.2.0) + dm-core (~> 1.2.0) + dm-transactions (1.2.0) + dm-core (~> 1.2.0) + dm-types (1.2.1) + bcrypt-ruby (~> 3.0.0) + dm-core (~> 1.2.0) + fastercsv (~> 1.5.4) + json (~> 1.6.1) + multi_json (~> 1.0.3) + stringex (~> 1.3.0) + uuidtools (~> 2.1.2) + dm-validations (1.2.0) + dm-core (~> 1.2.0) + do_sqlite3 (0.10.7) + data_objects (= 0.10.7) + extlib (0.9.15) + fastercsv (1.5.4) + json (1.6.4) + json_pure (1.6.4) + ken (0.2.1) + addressable + extlib + json + multi_json (1.0.4) + rack (1.4.0) + rack-protection (1.2.0) + rack + redis (2.2.2) + sinatra (1.3.2) + rack (~> 1.3, >= 1.3.6) + rack-protection (~> 1.2) + tilt (~> 1.3, >= 1.3.3) + stringex (1.3.0) + tilt (1.3.3) + tins (0.3.7) + uuidtools (2.1.2) + +PLATFORMS + ruby + +DEPENDENCIES + amatch + data_mapper + dm-sqlite-adapter + ken + rack + redis + sinatra diff --git a/README b/README new file mode 100644 index 0000000..3c8f43a --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +Web controller DVD player. + +This was written as a personal project. I have no plans for expanding it to a full-fledged project, I'm just posting it here to have it somewhere safe. diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..cbe356e --- /dev/null +++ b/config.ru @@ -0,0 +1,16 @@ +require File.join(File.dirname(__FILE__), 'sideshow/sideshow') + +Sideshow::Model.init(ENV['DB']) +Sideshow::App.init() + +map "/static" do + run Rack::File.new(File.join(File.dirname(__FILE__), 'static')) +end + +map "/control" do + run Sideshow::Controller +end + +map "/" do + run Sideshow::App +end diff --git a/mplayer/control.rb b/mplayer/control.rb new file mode 100644 index 0000000..0f54e5e --- /dev/null +++ b/mplayer/control.rb @@ -0,0 +1,143 @@ +module Mplayer + class Control + attr_accessor :command + attr_accessor :env + + def initialize(command, display = ":0") + self.command = command + self.env = { "DISPLAY" => display } + @open = false + end + + def open(file) + self.stop if @open + + @stdout, w = IO.pipe + r, @stdin = IO.pipe + @process = Process.spawn(self.env, self.command.sub("%DVD_IMAGE%", file), + :out => w, + :in => r, + :err => "/dev/null") + nil + @open = true + end + + def send_command(cmd) + if @open + begin + while true + @stdout.read_nonblock(1000) + end + rescue Errno::EAGAIN + # Do nothing + end + @stdin.puts(cmd) + sleep(0.25) + + result = '' + begin + while true + result += @stdout.read_nonblock(1000) + end + rescue + # Do nothing + end + + result.sub(/\n$/, "") + end + end + + def get_property(name) + result = self.send_command("pausing_keep_force get_property #{name}") + + if result.match("ANS_#{name}=") + property = Property.new(name) + property.mplayer_value = result.sub("ANS_#{name}=", "") + return property.to_object + else + return result # TODO: Throw an exception + end + end + + def set_property(name, value) + property = Property.new(name) + property.value = value; + + if property.set? + self.send_command("pausing_keep_force set_property #{name} #{property.mplayer_value}") + end + end + + def step_property(name, value = 0, direction = 1) + property = Property.new(name) + + if property.step? + self.send_command("pausing_keep_force step_property #{name} #{value} #{direction}") + end + end + + def nav(button) + self.send_command("dvdnav #{button.to_s}") + + case button + when :select + self.set_property(:sub_forced_only, true) + when :menu + self.set_property(:sub_forced_only, false) + end + end + + def pause + self.send_command("pause") + end + + def paused? + self.get_property(:pause) + end + + def stop + self.send_command("stop") + @open = false + end + + def chapter + self.get_property(:chapter) + end + + def chapter=(chapter) + self.set_property(:chapter, chapter) + end + + def chapters + self.get_property(:chapters) + end + + def pos + self.get_property(:time_pos) + end + + def pos=(pos) + self.set_property(:stream_time_pos, pos) # Returns a more accurate value, but can't be set + end + + def length + self.get_property(:length) + end + + def seek(value, method = :relative) + case method + when :relative + type = 0 + when :percent + type = 1 + when :absolute + type = 2 + end + self.send_command("seek #{value} #{type}") + end + + def seek_chapter(direction) + self.step_property(:chapter, 1, direction) + end + end +end diff --git a/mplayer/mplayer.rb b/mplayer/mplayer.rb new file mode 100644 index 0000000..efb9fb4 --- /dev/null +++ b/mplayer/mplayer.rb @@ -0,0 +1,2 @@ +require_relative 'property' +require_relative 'control' diff --git a/mplayer/property.rb b/mplayer/property.rb new file mode 100644 index 0000000..17953ae --- /dev/null +++ b/mplayer/property.rb @@ -0,0 +1,82 @@ +module Mplayer + class Property + def self.properties + { + :pause => { :type => :flag, :set => false, :step => false }, + :chapter => { :type => :int, :set => true, :step => true }, + :chapters => { :type => :int, :set => false, :step => false }, + :length => { :type => :time, :set => false, :step => false }, + :percent_pos => { :type => :int, :set => true, :step => true }, + :time_pos => { :type => :time, :set => true, :step => true }, + :stream_time_pos => { :type => :time, :set => false, :step => false }, + :sub_forced_only => { :type => :flag, :set => true, :step => true } + } + end + + attr_accessor :name + attr_accessor :value + attr_accessor :mplayer_value + + def initialize(name) + self.name = name + self.value = value + end + + def name=(name) + @name = name.to_sym + end + + def value + case Property.properties[self.name][:type] + when :int + return @value.to_i + when :float + return @value.to_f + when :flag + return (@value == "1") + when :time + return @value.to_i + else + return @value + end + end + + def value=(value) + case Property.properties[self.name][:type] + when :flag + @value = (value ? "1" : "0"); + else + @value = value.to_s + end + end + + def mplayer_value + @value + end + + def mplayer_value=(value) + case Property.properties[self.name][:type] + when :flag + @value = (value == "yes" ? "1" : "0") + else + @value = value + end + end + + def set? + Property.properties[self.name][:set] + end + + def step? + Property.properties[self.name][:step] + end + + def to_object + self.value + end + + def to_mplayer + self.mplayer_value + end + end +end diff --git a/sideshow/app.rb b/sideshow/app.rb new file mode 100755 index 0000000..4d35456 --- /dev/null +++ b/sideshow/app.rb @@ -0,0 +1,247 @@ +#!/usr/bin/env ruby + +require "rubygems" +require "bundler/setup" + +require "sinatra/base" + +require "ken" +require "amatch" + +require "net/https" +require "uri" + +require_relative "cache" +require_relative "util" +require_relative "model" + +module Sideshow + class App < Sinatra::Base + def self.init() + Cache.setup(Model::Setting.get(:cache_server), Model::Setting.get(:cache_prefix)) + + media_root = Model::Setting.get(:media_root) + unless media_root.nil? or media_root.empty? + Model::Movie.loadFiles(media_root) + end + + if Cache.enabled? + Model::Movie.all(:fields => [:resource], :unique => true, :order => nil, :resource.not => nil).each do |m| + Util.program_info(:id => m.resource) + end + end + end + + helpers do + def resource_url(resource) + Util.resource_url(resource) + end + + def image_tag(resource, height) + Util.image_tag(resource, height) + end + end + + get "/" do + Cache.get("page:program_list", 24 * 3600) do + movies = Model::Movie.all(:fields => [:resource], :unique => true, :order => nil, :resource.not => nil) + programs = [] + + if movies.length > 0 + programs = movies.map do |m| + Util.program_info(:id => m.resource) + end + + programs.sort_by! { |i| i[:label].sub(/(^the)\s(.*)$/i, "\\2, \\1") } + end + + erb :index, :locals => { + :title => "Movies", + :active => "list", + :content_class => "movies", + :programs => programs, + :unassociated => Model::Movie.unassociated + } + end + end + + get "/add" do + erb :add, :layout => :dialog_layout, :locals => { + :title => "Add Media", + :content_class => "add", + :resource => params[:resource], + :unassociated => Model::Movie.unassociated + } + end + + get "/doAdd" do + resource = Ken.get(params[:resource]) + + movie = Model::Movie.get(params[:media]) + priority = Model::Movie.max(:priority, :conditions => ['resource = ?', resource.id]) or 0 + + movie.resource = resource.id + movie.description = params[:description] + movie.priority = priority + 1 + movie.save + + Cache.evict("program:#{resource.id}") + Cache.evict("page:program_list") + + status 201 + end + + get "/search" do + Cache.get("page:search", 24 * 3600) do + erb :search, :locals => { + :title => "Search", + :active => "search", + :content_class => "search" + } + end + end + + get "/doSearch" do + results = [] + unless params[:search].nil? + name_matches = Ken.all("name~=" => params[:search], "type|=" => ["/film/film", "/tv/tv_program"]) + alias_matches = Ken.all("/common/topic/alias~=" => params[:search], "type|=" => ["/film/film", "/tv/tv_program"]) + + results.concat(name_matches) + results.concat(alias_matches) + results.uniq! { |r| r.id } + + matcher = Amatch::Levenshtein.new(params[:search]) + + results.map! do |r| + names = [r.name] + aliases = r.attribute("/common/topic/alias") + names.concat(aliases.values) unless aliases.nil? + + { :distance => matcher.match(names).min }.merge(Util.program_info(:program => r)) + end + + results.sort! do |a, b| + value = a[:distance] <=> b[:distance] + if value == 0 + value = a[:label] <=> b[:label] + end + value + end + end + + erb :search_results, :layout => false, :locals => { + :results => results + } + end + + get "/settings" do + Cache.get("page:settings", 24 * 3600) do + erb :settings, :layout => :dialog_layout, :locals => { + :title => "Settings", + :content_class => "settings", + :media_root => "", + :cache_server => "" + }.merge(Model::Setting.getAll) + end + end + + get "/updateModel::Settings" do + old_values = Model::Setting.getAll + cache_reconnect = false + + params.each do |k, v| + Model::Setting.set(k, v) + + if k == "media_root" and v != old_values[:media_root] + Model::Movie.loadFiles(v) + end + + if k == "cache_server" and v != old_values[:cache_server] + cache_reconnect = true + end + + if k == "cache_prefix" and v != old_values[:cache_prefix] + cache_reconnect = true + end + end + + Cache.setup(Model::Setting.get(:cache_server), Model::Setting.get(:cache_prefix)) if cache_reconnect + + Cache.evict("page:settings") + + status 201 + end + + get "/refresh" do + root = Model::Setting.get(:media_root) + Model::Movie.loadFiles(root) + + Cache.evict("page:program_list") + + status 201 + end + + get "/flush" do + Cache.flush + + status 201 + end + + get "/remote" do + Cache.get("page:remote", 24 * 3600) do + erb :remote, :layout => :dialog_layout, :locals => { + :title => "Remote", + :content_class => "remote" + } + end + end + + get %r{/program(/.+)} do |id| + info = Util.program_info(:id => id) + + erb :program, :locals => { + :title => info[:label], + :active => "", + :content_class => "program" + }.merge(info) + end + + get %r{/movies(/.+)} do |id| + erb :movie_list, :layout => false, :locals => Util.program_info(:id => id) + end + + get %r{/image/(\d+)(/.+)} do |size, id| + begin + data = Cache.get("image:#{id}@#{size}", 1..7 * 24 * 3600) do + uri = URI.parse("https://usercontent.googleapis.com/freebase/v1/image#{id}?mode=fit&maxheight=#{size}") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + request = Net::HTTP::Get.new(uri.request_uri) + + response = http.request(request) + + raise "Request Fail" unless response.is_a? Net::HTTPSuccess + + data = { + :headers => { "Content-Type" => response.content_type }, + :content => response.body + } + end + + headers data[:headers] + data[:content] + rescue RuntimeError => e + if e.message == "Request Fail" + status 500 + else + raise + end + end + end + + run! if app_file == $0 + end +end diff --git a/sideshow/cache.rb b/sideshow/cache.rb new file mode 100644 index 0000000..efc2a6c --- /dev/null +++ b/sideshow/cache.rb @@ -0,0 +1,53 @@ +require 'redis' + +module Sideshow + class Cache + def self.setup(address, prefix) + @prefix = prefix + @cache = nil + + unless address.nil? or address.empty? + host, port = address.split(':') + @cache = Redis.new(:host => host, :port => port) + end + end + + def self.enabled? + not @cache.nil? + end + + def self.get(key, timeout=0) + key = "#{@prefix}:#{key}" + if self.enabled? + value = @cache.get(key) + if value.nil? + value = yield + @cache.set(key, Marshal::dump(value)) + + timeout = Random.rand(timeout) if timeout.is_a? Range + @cache.expire(key, timeout) unless timeout == 0 + else + value = Marshal::load(value) + end + + return value + else + yield + end + end + + def self.evict(key) + if self.enabled? + key = "#{@prefix}:#{key}" + @cache.del(key) + end + end + + def self.flush + if self.enabled? + keys = @cache.keys("#{@prefix}:*") + @cache.del(*keys) + end + end + end +end diff --git a/sideshow/controller.rb b/sideshow/controller.rb new file mode 100644 index 0000000..234740b --- /dev/null +++ b/sideshow/controller.rb @@ -0,0 +1,88 @@ +require "sinatra/base" + +require_relative "../mplayer/mplayer" +require_relative "model" + +module Sideshow + class Controller < Sinatra::Base + def self.mplayer + @mplayer ||= nil + end + + def self.mplayer=(mplayer) + @mplayer = mplayer + end + + get "/play" do + movie = Model::Movie.get(params[:id]) + root = Model::Setting.get(:media_root) + + Controller.mplayer.stop unless Controller.mplayer.nil? + + Controller.mplayer = Mplayer::Control.new(Model::Setting.get(:player_cmd)) + Controller.mplayer.open(File.join(root, movie.file)) + status 201 + end + + get "/pause" do + Controller.mplayer.pause unless Controller.mplayer.nil? + status 201 + end + + get "/stop" do + Controller.mplayer.stop unless Controller.mplayer.nil? + Controller.mplayer = nil + status 201 + end + + get "/up" do + Controller.mplayer.nav :up unless Controller.mplayer.nil? + status 201 + end + + get "/down" do + Controller.mplayer.nav :down unless Controller.mplayer.nil? + status 201 + end + + get "/left" do + Controller.mplayer.nav :left unless Controller.mplayer.nil? + status 201 + end + + get "/right" do + Controller.mplayer.nav :right unless Controller.mplayer.nil? + status 201 + end + + get "/select" do + Controller.mplayer.nav :select unless Controller.mplayer.nil? + status 201 + end + + get "/menu" do + Controller.mplayer.nav :menu unless Controller.mplayer.nil? + status 201 + end + + get "/chapter_back" do + Controller.mplayer.seek_chapter(-1) unless Controller.mplayer.nil? + status 201 + end + + get "/chapter_fwd" do + Controller.mplayer.seek_chapter(1) unless Controller.mplayer.nil? + status 201 + end + + get "/skip_back" do + Controller.mplayer.seek(-30) unless Controller.mplayer.nil? + status 201 + end + + get "/skip_fwd" do + Controller.mplayer.seek(30) unless Controller.mplayer.nil? + status 201 + end + end +end diff --git a/sideshow/model.rb b/sideshow/model.rb new file mode 100644 index 0000000..b72f866 --- /dev/null +++ b/sideshow/model.rb @@ -0,0 +1,71 @@ +require "data_mapper" + +module Sideshow + module Model + def self.init(db) + DataMapper.setup(:default, db) + DataMapper.finalize + DataMapper.auto_upgrade! + + if Setting.get(:cache_prefix).nil? + Setting.set(:cache_prefix, "default") + end + + if Setting.get(:player_cmd).nil? + Setting.set(:player_cmd, "/usr/bin/mplayer -fs -slave -quiet dvdnav:///%DVD_IMAGE%") + end + end + + class Movie + include DataMapper::Resource + + storage_names[:default] = "movies" + + property :id, Serial + property :priority, Integer, :default => 0 + property :description, String + property :file, String, :length => 255, :unique => true + property :resource, String, :length => 255 + + def self.loadFiles(root) + media_glob = File.join(root, "*.iso") + Dir.glob(media_glob).each do |file| + Movie.create(:file => file.sub(root, "").sub(/^\//, "")) + end + end + + def self.unassociated + Movie.all(:resource => nil, :order => [:file.asc]) + end + end + + class Setting + include DataMapper::Resource + + storage_names[:default] = "settings" + + property :name, String, :unique => true, :key => true + property :value, String, :length => 255 + + def self.getAll() + result = {} + Setting.all.each do |s| + result[s.name.to_sym] = Marshal::load(s.value) + end + + result + end + + def self.get(name) + s = Setting.first(:name => name.to_s) + Marshal::load(s.value) unless s.nil? + end + + def self.set(name, value) + s = Setting.first_or_create(:name => name.to_s) + s.value = Marshal::dump(value) + s.save + end + end + end +end diff --git a/sideshow/sideshow.rb b/sideshow/sideshow.rb new file mode 100644 index 0000000..aced579 --- /dev/null +++ b/sideshow/sideshow.rb @@ -0,0 +1,5 @@ +require_relative 'util' +require_relative 'model' +require_relative 'cache' +require_relative 'app' +require_relative 'controller' diff --git a/sideshow/util.rb b/sideshow/util.rb new file mode 100644 index 0000000..db38946 --- /dev/null +++ b/sideshow/util.rb @@ -0,0 +1,122 @@ +require "net/https" +require "uri" +require "json" + +module Sideshow + class Util + def self.resource_url(resource) + "/program#{resource.id}" + end + + def self.image_tag(resource, height) + url = "/image/#{height}#{resource.id}" + "\"#{resource.name}\"" + end + + def self.get_article(resource) + uri = URI.parse("https://www.googleapis.com/freebase/v1/text#{resource.id}?format=html") + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + + request = Net::HTTP::Get.new(uri.request_uri) + + response = http.request(request) + + JSON.parse(response.body)["result"] + end + + def self.format_date(date, format="%b %e, %Y") + begin + t = Time.parse(date) + t.strftime(format) + rescue + date + end + end + + def self.resource_type(resource) + type = "film" unless resource.type("/film/film").nil? + type = "tv" unless resource.type("/tv/tv_program").nil? + + return type + end + + def self.resource_type_label(type) + case type + when "film" + return "Movie" + when "tv" + return "TV Series" + end + end + + def self.program_info(opts = {}) + program = opts[:program].nil? ? nil : opts[:program] + id = opts[:id].nil? ? program.id : opts[:id] + + Cache.get("program:#{id}", 1..7 * 24 * 3600) do + program = Ken.get(id) if program.nil? + type = self.resource_type(program) + + info = { + :program => program, + :label => program.name, + :summary => self.get_article(program), + :type => self.resource_type_label(type), + :release_year => nil, + :details => {}, + :movies => Model::Movie.all(:resource => program.id, :order => [:priority.asc]) + } + + case type + when "film" + released = program.attribute("/film/film/initial_release_date") + info[:details]["Released"] = self.format_date(released.values[0]) unless released.nil? + info[:release_year] = self.format_date(released.values[0], "%Y") unless released.nil? + + rating = program.attribute("/film/film/rating") + info[:details]["Rating"] = self.image_tag(rating.values[0], 20) unless rating.nil? + + film_runtime = program.attribute("/film/film/runtime") + unless film_runtime.nil? + cut_runtime = film_runtime.values[0].attribute("/film/film_cut/runtime") + unless cut_runtime.nil? + runtime = cut_runtime.values[0].to_i + info[:details]["Runtime"] = "#{runtime / 60}h#{runtime % 60}m" + end + end + + genres = program.attribute("/film/film/genre") + unless genres.nil? + genre_items = genres.values.map {|g| "
  • #{g.name}
  • " } + info[:details]["Genres"] = "" + end + when "tv" + first_episode = program.attribute("/tv/tv_program/air_date_of_first_episode") + first_episode_date = first_episode.nil? ? "unknown" : first_episode.values[0] + + final_episode = program.attribute("/tv/tv_program/air_date_of_final_episode") + final_episode_date = final_episode.nil? ? "unknown" : final_episode.values[0] + + info[:details]["Aired"] = "#{self.format_date(first_episode_date)} - #{self.format_date(final_episode_date)}" + info[:release_year] = "#{self.format_date(first_episode_date, "%Y")} - #{self.format_date(final_episode_date, "%Y")}" + + seasons = program.attribute("/tv/tv_program/seasons") + info[:details]["Seasons"] = seasons.values.length unless seasons.nil? + + episodes = program.attribute("/tv/tv_program/episodes") + info[:details]["Episodes"] = episodes.values.length unless episodes.nil? + + genres = program.attribute("/tv/tv_program/genre") + unless genres.nil? + genre_items = genres.values.map {|g| "
  • #{g.name}
  • " } + info[:details]["Genres"] = "" + end + end + + info + end + end + end +end diff --git a/sideshow/views/add.erb b/sideshow/views/add.erb new file mode 100644 index 0000000..21d580b --- /dev/null +++ b/sideshow/views/add.erb @@ -0,0 +1,19 @@ +
    + + +
    + + +
    + +
    + + +
    + + +
    diff --git a/sideshow/views/dialog_layout.erb b/sideshow/views/dialog_layout.erb new file mode 100644 index 0000000..781f349 --- /dev/null +++ b/sideshow/views/dialog_layout.erb @@ -0,0 +1,16 @@ + + + +
    + +
    +

    <%= title %>

    +
    + +
    + <%= yield %> +
    + +
    + + diff --git a/sideshow/views/index.erb b/sideshow/views/index.erb new file mode 100644 index 0000000..f581316 --- /dev/null +++ b/sideshow/views/index.erb @@ -0,0 +1,13 @@ +<% if unassociated.length > 0 %> +
    +

    <%= unassociated.length %> Unassociated Files

    + + +
    +<% end %> + +<%= erb :program_list, :layout => false, :locals => { :programs => programs } %> diff --git a/sideshow/views/layout.erb b/sideshow/views/layout.erb new file mode 100644 index 0000000..40d42ae --- /dev/null +++ b/sideshow/views/layout.erb @@ -0,0 +1,37 @@ + + + + Sideshow + + + + + + + + + + + +
    + +
    +

    <%= title %>

    +
    + +
    + <%= yield %> +
    + +
    +
    +
    + +
    + + diff --git a/sideshow/views/movie_list.erb b/sideshow/views/movie_list.erb new file mode 100644 index 0000000..d961f6a --- /dev/null +++ b/sideshow/views/movie_list.erb @@ -0,0 +1,8 @@ +
    + <% movies.each do |m| %> + <%= m.description %> + <% end %> + + Add Media + +
    diff --git a/sideshow/views/program.erb b/sideshow/views/program.erb new file mode 100644 index 0000000..27dbfe4 --- /dev/null +++ b/sideshow/views/program.erb @@ -0,0 +1,21 @@ +
    +
    <%= image_tag(program, 200) %>
    + <%= summary %> + Full Details +
    + +
    +
    +
    + <% details.each_pair do |k, v| %> +
    <%= k %>
    +
    <%= v %>
    + <% end %> +
    +
    + +
    + <%= erb :movie_list, :layout => false, :locals => { :program => program, :movies => movies } %> +
    +
    + diff --git a/sideshow/views/program_list.erb b/sideshow/views/program_list.erb new file mode 100644 index 0000000..1291562 --- /dev/null +++ b/sideshow/views/program_list.erb @@ -0,0 +1,11 @@ + diff --git a/sideshow/views/remote.erb b/sideshow/views/remote.erb new file mode 100644 index 0000000..26ab60d --- /dev/null +++ b/sideshow/views/remote.erb @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + +
    + Up +
    + Left + + Select + Menu + + Right +
    + Down +
    + +
    + Chapter + 30 sec. + 30 sec. + Chapter +
    + +
    + Pause + Stop +
    diff --git a/sideshow/views/search.erb b/sideshow/views/search.erb new file mode 100644 index 0000000..23c110c --- /dev/null +++ b/sideshow/views/search.erb @@ -0,0 +1,8 @@ +
    +
    + + +
    +
    + +
    diff --git a/sideshow/views/search_results.erb b/sideshow/views/search_results.erb new file mode 100644 index 0000000..dd3987c --- /dev/null +++ b/sideshow/views/search_results.erb @@ -0,0 +1,5 @@ +<% if results.length > 0 %> + <%= erb :program_list, :layout => false, :locals => { :programs => results } %> +<% else %> + No results found +<% end %> diff --git a/sideshow/views/settings.erb b/sideshow/views/settings.erb new file mode 100644 index 0000000..071cc95 --- /dev/null +++ b/sideshow/views/settings.erb @@ -0,0 +1,27 @@ +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + Refresh Media + Flush Cache +
    +
    diff --git a/static/css/sideshow.css b/static/css/sideshow.css new file mode 100644 index 0000000..ac644f5 --- /dev/null +++ b/static/css/sideshow.css @@ -0,0 +1,74 @@ +.ui-field-contain label.ui-input-text { + vertical-align: middle; +} + +/**** Programs ****/ + +.program .cover-art { + float: left; + margin-right: 15px; +} + +.program p { + margin-top: 0; +} + +.program dl { + clear: both; + margin: 0; + padding: 15px 0; +} + +.program dl dt { + float: left; + clear: both; + width: 100px; + font-weight: bold; + margin-bottom: 10px; +} + +.program dl dd { + float: left; +} + +.program dl dd ul { + display: inline; + overflow: hidden; + margin: 0; + padding: 0; + list-style-type: none; +} + +/**** Settings Panel ****/ + +.settings .ui-controlgroup { + text-align: center; +} + +/**** Remote Control ****/ + +.remote { + text-align: center; +} + +.remote table { + margin: 0 auto; +} + +.remote table td { + padding: 5px; +} + +.remote table .single a { + height: 76px; + width: 105px; +} + +.remote table .single a .ui-btn-inner { + position: absolute; + top: 50%; + left: 50%; + margin-left: -9px; + margin-top: -9px; + padding: 0; +} diff --git a/static/js/sideshow.js b/static/js/sideshow.js new file mode 100644 index 0000000..5e5892b --- /dev/null +++ b/static/js/sideshow.js @@ -0,0 +1,113 @@ +$('[data-role="page"], [data-role="dialog"]').live('pageinit', function(evt) { + var page = $(evt.target); + + page.find('a[href^="/control/"]').click(function() { + $.mobile.showPageLoadingMsg(); + + $.ajax({ + type: 'GET', + url: $(this).attr('href'), + complete: function() { + $.mobile.hidePageLoadingMsg(); + } + }); + + return false; + }); +}); + +$('.search').live('pageinit', function(evt) { + var page = $(evt.target), + results = page.find('.results'); + + page.find('form').submit(function() { + var form = $(this); + + $.mobile.showPageLoadingMsg(); + + $.ajax({ + type: 'GET', + url: form.attr('action'), + data: form.serialize(), + complete: function(xhr) { + $.mobile.hidePageLoadingMsg(); + results.html(xhr.responseText); + results.trigger('create'); + } + }); + + return false; + }); +}); + +$('.add').live('pageinit', function(evt) { + var page = $(evt.target); + + page.find('form').submit(function() { + var form = $(this); + + $.mobile.showPageLoadingMsg(); + + $.ajax({ + type: 'GET', + url: form.attr('action'), + data: form.serialize(), + complete: function() { + var id = form.find('[name="resource"]').val(), + movie_list = $('[data-movies-for="' + id + '"]'); + + $('.movies').remove(); + + $.ajax({ + type: 'GET', + url: '/movies' + id, + complete: function(xhr) { + movie_list.html(xhr.responseText); + movie_list.trigger('create'); + + $.mobile.hidePageLoadingMsg(); + page.dialog('close'); + } + }); + } + }); + + return false; + }); +}); + +$('.settings').live('pageinit', function(evt) { + var page = $(evt.target); + + page.find('form').submit(function() { + var form = $(this); + + $.mobile.showPageLoadingMsg(); + + $.ajax({ + type: 'GET', + url: form.attr('action'), + data: form.serialize(), + complete: function() { + $.mobile.hidePageLoadingMsg(); + page.dialog('close'); + } + }); + + return false; + }); + + page.find('a[href="/refresh"], a[href="/flush"]').click(function() { + $.mobile.showPageLoadingMsg(); + + $.ajax({ + url: $(this).attr('href'), + complete: function() { + $.mobile.hidePageLoadingMsg(); + page.dialog('close'); + } + }); + + return false; + }); +});