<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>lib/mythtv/setting.rb</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -1,3 +1,8 @@
+== 0.3.0 (2009-02-18)
+
+* Replaced old database code with one based on ActiveRecord, which is now a dependency
+* Changed test code
+
 == 0.2.0 (2008-09-29)
 
 * Bumped to version 0.2 as we now support proper editing of recording schedules, and speaking multiple versions of the database schema</diff>
      <filename>History.txt</filename>
    </modified>
    <modified>
      <diff>@@ -3,29 +3,35 @@ require 'rake/gempackagetask'
 require 'rake/testtask'
 
 spec = Gem::Specification.new do |s|
-  s.name = %q{ruby-mythtv}
-  s.version = &quot;0.2.0&quot;
+  s.name = 'ruby-mythtv'
+  s.version = '0.3.0'
  
   s.specification_version = 2 if s.respond_to? :specification_version=
  
-  s.required_rubygems_version = Gem::Requirement.new(&quot;&gt;= 0&quot;) if s.respond_to? :required_rubygems_version=
-  s.authors = [&quot;Nick Ludlam&quot;]
-  s.date = %q{2008-07-27}
-  s.description = %q{Ruby implementation of the MythTV communication protocol}
+  s.required_rubygems_version = Gem::Requirement.new('&gt;= 0') if s.respond_to? :required_rubygems_version=
+  s.date = %q{2009-02-18}
+  s.description = %q{Ruby implementation of the MythTV communication protocol, and interface to the MythTV database}
+  s.authors = [ 'Nick Ludlam' ]
   s.email = %q{nick@recoil.org}
-  s.extra_rdoc_files = [&quot;History.txt&quot;, &quot;License.txt&quot;, &quot;README.txt&quot;]
-  s.files = [&quot;History.txt&quot;, &quot;License.txt&quot;, &quot;README.txt&quot;, &quot;Rakefile&quot;, &quot;lib/ruby-mythtv.rb&quot;, &quot;lib/mythtv/backend.rb&quot;, &quot;lib/mythtv/channel.rb&quot;, &quot;lib/mythtv/database.rb&quot;, &quot;lib/mythtv/program.rb&quot;, &quot;lib/mythtv/protocol.rb&quot;, &quot;lib/mythtv/recording.rb&quot;, &quot;lib/mythtv/recording_schedule.rb&quot;, &quot;lib/mythtv/utils.rb&quot;, &quot;test/test_backend.rb&quot;, &quot;test/test_db.rb&quot;, &quot;test/test_helper.rb&quot;]
+  s.extra_rdoc_files = [ 'History.txt', 'License.txt', 'README.txt', 'Todo.txt' ]
+  s.files = [ 'History.txt', 'License.txt', 'README.txt', 'Rakefile', 'Todo.txt' ] + Dir[&quot;lib/*.rb&quot;] + Dir[&quot;lib/mythtv/*.rb&quot;] + Dir[&quot;test/*.rb&quot;]
   s.has_rdoc = true
   s.homepage = %q{http://github.com/nickludlam/ruby-mythtv/}
-  s.rdoc_options = [&quot;--main&quot;, &quot;README.txt&quot;]
-  s.require_paths = [&quot;lib&quot;]
+  s.rdoc_options = ['--main', 'README.txt']
+  s.require_paths = ['lib']
   s.rubyforge_project = %q{ruby-mythtv}
-  s.rubygems_version = %q{0.2.0}
+  s.rubygems_version = %q{0.3.0}
+  
+  s.add_dependency('mysql')
+  s.add_dependency('activerecord')
+  s.add_dependency('composite_primary_keys')
+  
   s.summary = %q{Ruby implementation of the MythTV backend protocol}
-  s.test_files = [&quot;test/test_backend.rb&quot;, &quot;test/test_helper.rb&quot;]
+  s.test_files = Dir[&quot;test/*.rb&quot;]
 end
 
 Rake::GemPackageTask.new(spec) do |pkg| 
+  pkg.need_zip = true
   pkg.need_tar = true 
 end 
 
@@ -34,18 +40,22 @@ task :build =&gt; &quot;pkg/#{spec.name}-#{spec.version}.gem&quot; do
 end
 
 desc &quot;Run basic unit tests&quot;
-Rake::TestTask.new(&quot;test_units&quot;) do |t|
+Rake::TestTask.new(&quot;test&quot;) do |t|
   t.pattern = ENV[&quot;TESTFILES&quot;] || ['test/test_backend.rb', 'test/test_db.rb']
   t.verbose = true
   t.warning = true
 end
 
-task :test =&gt; :test_units
-
-Rake::TestTask.new('test_db') do |t|
+Rake::TestTask.new('test:db') do |t|
   t.pattern = ['test/test_db.rb']
   t.verbose = true
 end
+
+Rake::TestTask.new('test:backend') do |t|
+  t.pattern = ['test/test_backend.rb']
+  t.verbose = true
+end
+
   
 desc &quot;Run unit tests as default&quot;
-task :default =&gt; :test_units
+task :default =&gt; :test</diff>
      <filename>Rakefile</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,7 @@
-- Implement the Recorder class, and associated functions (see existing MythTV Python module)
-- Look at how we obtain the channel icon by streaming a backend file (see existing MythTV Perl module)
-- Support Ruby 1.9
-- Support seeking with the MythTV::Backend#stream() method
-- Look at how we need to pass the DB instance to each constructor for Channel, Program and RecordingSchedule. Automate the link back to db instance?
-- Make use of the ENUMS placed in RecordingSchedule to help with validation of various columns
-- Introspect column types in the database, and coerce the values into the correct class. Currently recordid is a string, for instance
+== Todo
+
+* Implement the Recorder class, and associated functions (see existing MythTV Python module)
+* Look at how we obtain the channel icon by streaming a backend file (see existing MythTV Perl module)
+* Support Ruby 1.9
+* Support seeking with the MythTV::Backend#stream() method
+* Make use of the ENUMS placed in RecordingSchedule to help with validation of various columns</diff>
      <filename>Todo.txt</filename>
    </modified>
    <modified>
      <diff>@@ -22,6 +22,14 @@ module MythTV
                 :filetransfer_size,
                 :socket
     
+    # Create a new instance of the backend connection, yield it into the block, then
+    # close once the block execution has finished
+    def self.perform_commands(initialize_options = {}, &amp;block)
+      connection = self.new(initialize_options)
+      yield connection
+      connection.close
+    end
+    
     # Open the socket, make a protocol check, and announce we'd like an interactive
     # session with the backend server.
     #
@@ -275,7 +283,7 @@ module MythTV
     # Tell the backend we've finished talking to it for the current session
     def close
       send(&quot;DONE&quot;)
-      @socket.close unless @socket.nil?
+      @socket.close
     end
     
     ############################################################################</diff>
      <filename>lib/mythtv/backend.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,27 +1,11 @@
 module MythTV
   
-  class Channel
-    # Construct a new instamce by passing in an array of elements, whos length,
-    # and order are the same as those set out in the columns mapping for Channel
-    # 
-    def initialize(channel_array, db_instance)
-      # Find out the attributes this class has from the schema calculations earlier
-      columns = db_instance.table_columns[self.class]
-      
-      self.class.class_eval { attr_accessor(*columns) }
-      
-      @columns = columns
-      
-      @columns.each_with_index do |col, i|
-        self.send(&quot;#{col}=&quot;, channel_array[i])
-      end
-      
-      @db = db_instance
-    end
-  
-    # Create a to_s method to help with debugging
-    def to_s; @columns.collect { |v| &quot;#{v}: '#{send(v) || 'nil'}'&quot; }.join(&quot;, &quot;); end
-    
+  class Channel &lt; ActiveRecord::Base
+    set_table_name 'channel'
+    set_primary_key :chanid
+
+    #has_many :recording_schedules, :foreign_key =&gt; 'chanid'
+    has_many :programs, :foreign_key =&gt; 'chanid', :class_name =&gt; &quot;MythTV::Program&quot;
   end
   
 end
\ No newline at end of file</diff>
      <filename>lib/mythtv/channel.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,256 +1,63 @@
 require 'rubygems'
+require 'active_record'
+require 'composite_primary_keys'
 require 'mysql'
 
 module MythTV
   
   class Database
-
-    attr_accessor :connection     # The MySQL connection object
-    attr_accessor :setting_cache  # The Hash which stores previously retrieved settings
-    attr_accessor :table_columns  # The Hash which contains the column info for the tables
-    
-    TABLE_TO_CLASS_MAP = { 'channel' =&gt; Channel,
-                           'program' =&gt; Program,
-                           'record'  =&gt; RecordingSchedule }
     
     # Initialise and connect to the MySQL server
     #
     # Required keys in options[] are:
     #
-    # :database_user =&gt; The username to connect to the mysql database as
-    # :database_password =&gt; The password to connect to the mysql database with
+    # :database_host (or :host) =&gt; The target server address or name
+    # :database_user            =&gt; The username used to connect to the MythTV MySQL database
+    # :database_password        =&gt; The password used to connect to the MythTV MySQL database
     #
     # Optional keys:
     #
-    # :database_host =&gt; Defaults to the same value as the backend host, unless specified
     # :database_name =&gt; Defaults to 'mythconverg' unless specified
     # :log_output    =&gt; A Ruby Logger output. Defaults to STDERR
     # :log_level     =&gt; A Ruby Logger log level. Defaults to Logger::WARN
     def initialize(options)
-      # Initialise the caches for later use
-      @setting_cache = {}
-      
-      default_options = { :database_user =&gt; 'mythtv', :database_name =&gt; 'mythconverg', :database_host =&gt; :host }
-      
-      raise ArgumentError, &quot;:database_password must be a key within the options argument Hash&quot; unless options.has_key?(:database_password)
+      default_options = { :database_user =&gt; 'mythtv',
+                          :database_name =&gt; 'mythconverg',
+                          :database_host =&gt; :host,
+                          :database_port =&gt; 3306 }
       
       options = default_options.merge(options)
       
       @database_host = options[:database_host] == :host ? options[:host] : options[:database_host]
       @database_name = options[:database_name]
       @database_user = options[:database_user]
+      @database_port = options[:database_port]
       @database_password = options[:database_password]
       
-      @connection = Mysql.real_connect(@database_host, @database_user, @database_password, @database_name)
-      
-      # Set up a local logging object
-      @log = MythTV::Utils.setup_logging(options)
-      
-      # Query the schema version, and calculate the columns in Channel, Program, and RecordingSchedule
-      @table_columns = {
-        Channel =&gt; process_dbschemaver(:channel),
-        Program =&gt; process_dbschemaver(:program),
-        RecordingSchedule =&gt; process_dbschemaver(:recording_schedule)
+      @connection_details = {
+        :adapter =&gt; 'mysql',
+        :host =&gt; @database_host,
+        :port =&gt; @database_port,
+        :database =&gt; @database_name,
+        :username =&gt; @database_user,
+        :password =&gt; @database_password
       }
-    end
-    
-    # Close the database connection properly
-    def close()
-      @connection.close() if @connection
-    end
-    
-    # Fetch the stored setting from the settings database table, for the specified value field
-    def get_setting(value, hostname = '')
-      # Construct a unique key. Fetch from cache if present
-      key = value + &quot;_&quot; + hostname
-      return @setting_cache[key] if @setting_cache.has_key?(key)
-      
-      query = &quot;SELECT data FROM settings WHERE value = ?&quot;
-      # If we're explicitly given a value for hostname, then make this part of
-      # the WHERE condition
-      query += &quot; AND hostname = ?&quot; unless hostname == ''
-
-      st = @connection.prepare(query)
-
-      begin
-        hostname == '' ? st.execute(value) : st.execute(value, hostname)
-        # We shouldn't get multiple matches, so check and raise an exception if necessary
-        raise ArgumentError, (&quot;Too many matches! %d matching rows found for '%s'&quot; % [st.num_rows, value]) if st.num_rows &gt; 1
-
-        # If we haven't found anything, return now
-        data = st.num_rows == 0 ? nil : st.fetch[0]
-      ensure
-        # Make sure we close the statement object nicely
-        st.close
-      end
-
-      # Set in the cache before we return the value
-      @setting_cache[key] = data
-      
-      data
-    end
-    
-    # Return an array of Channel objects, selected by the criteria set out in
-    # the options hash.
-    #
-    def list_channels(options = {})
-      default_options = { :order =&gt; &quot;chanid ASC&quot; }
-      
-      # Merge in our defaults with what we've been given
-      options = default_options.merge(options)
-      
-      st_query = &quot;SELECT &quot; + @table_columns[Channel].collect { |c| c.to_s }.join(&quot;,&quot;) + &quot; FROM channel&quot;
-      
-      (converted_query, st_args) = simple_options_to_sql(options, 'channel')
-      st_query += converted_query
-      
-      @log.debug(&quot;CHANNEL QUERY: #{st_query}&quot;)
-      
-      # Execute the statement, and create the Channel objects from the results
-      st = @connection.prepare(st_query)
-      results = st.execute(*st_args)
-      channels = []
-      st.num_rows.times { channels &lt;&lt; Channel.new(st.fetch, self) }
-      
-      channels
-    end
-    
-    def list_programs(options = {})
-      default_options = { :order =&gt; &quot;starttime ASC&quot; }
-      
-      # Merge in our defaults with what we've been given
-      options = default_options.merge(options)
-      
-      st_query = &quot;SELECT &quot; + @table_columns[Program].collect { |c| c.to_s }.join(&quot;,&quot;) + &quot; FROM program&quot;
-      
-      (converted_query, st_args) = simple_options_to_sql(options, 'program')
-      st_query += converted_query
       
-      @log.debug(&quot;PROGRAM QUERY: #{st_query}&quot;)
+      # Establish our connections specifically for the ruby-mythtv module
+      # TODO: I'm not particularly happy about this, as I'd rather it was done
+      #       on demand, but at the moment it's not clear how you share nicely
+      #       with other ActiveRecord instances talking to other databases
+      MythTV::Channel.establish_connection(@connection_details)
+      MythTV::Program.establish_connection(@connection_details)
+      MythTV::RecordingSchedule.establish_connection(@connection_details)
+      MythTV::Setting.establish_connection(@connection_details)
       
-      # Execute the statement, and create the Channel objects from the results
-      st = @connection.prepare(st_query)
-      results = st.execute(*st_args)
-      programs = []
-      st.num_rows.times { programs &lt;&lt; Program.new(st.fetch, self) }
-      
-      programs
-    end
-    
-    def list_recording_schedules(options = {})
-      default_options = { :order =&gt; &quot;recordid ASC&quot; }
-      
-      # Merge in our defaults with what we've been given
-      options = default_options.merge(options)
-      
-      st_query = &quot;SELECT &quot; + @table_columns[RecordingSchedule].collect { |c| c.to_s }.join(&quot;,&quot;) + &quot; FROM record&quot;
-      
-      (converted_query, st_args) = simple_options_to_sql(options, 'record')
-      st_query += converted_query
-      
-      @log.debug(&quot;RECORD QUERY: #{st_query}&quot;)
-      
-      # Execute the statement, and create the Channel objects from the results
-      st = @connection.prepare(st_query)
-      results = st.execute(*st_args)
-      recording_schedules = []
-      st.num_rows.times { recording_schedules &lt;&lt; RecordingSchedule.new(st.fetch, self) }
-      
-      recording_schedules
-    end
-  
-  private
-    # We need to work out what schema version the database is, and derive the columns
-    # present for each of the database tables represented in Channel, Program and RecordingSchedule
-    def process_dbschemaver(class_name_sym)
-      # First get the DBSchemaVer of the database we're talking to
-      schema_version = get_setting('DBSchemaVer').to_i
-      
-      #@log.debug(&quot;Got schema_version #{schema_version}&quot;)
-
-      base_columns_const_name = class_name_sym.to_s.upcase + &quot;_BASE_COLUMNS&quot;
-      # Duplicate this for schema change processing
-      table_columns = MythTV.const_get(base_columns_const_name).dup
-
-      #@log.debug(&quot;base table_columns are: #{table_columns.inspect}&quot;)
-      
-      # Find any schema changes for this version of the DBSchemaVer
-      schema_changes_const_name = class_name_sym.to_s.upcase + &quot;_SCHEMA_CHANGES&quot;
-      schema_changes = MythTV.const_get(schema_changes_const_name)
-      
-      # Process the :push and :remove keys appropriately
-      if schema_changes.has_key?(schema_version)
-        @log.debug(&quot;Schema mapping found. Applying changes to the column layout&quot;)
-        schema_changes[schema_version].each_pair do |action, columns|
-          columns.map do |column|
-            table_columns.send(action, column)
-          end
-        end
-      end
-
-      #@log.debug(&quot;Returning column array #{table_columns.inspect}&quot;)
-      table_columns
-    end
-  
-  
-    # A simple method which allows the presence of a key in the options array which matches
-    # a column in the table to be turned into a simple equality statement, or IN statement.
-    #
-    # If the value class is an array, the resulting statement is of the form &quot;&lt;col&gt; IN (?, ?...)&quot;
-    #
-    # In all other cases, the resulting statement is of the form &quot;&lt;co&gt; = ?&quot;
-    #
-    # Also allows specification of :conditions, :order and :limit, which emulate their
-    # ActiveRecord counterparts
-    def simple_options_to_sql(options, table_name)
-      where_query = []     # Accumulate statements here, and join with &quot; AND &quot; after
-      where_args  = []     # Accumulate substitution variables here
-      assembled_query = &quot;&quot; # Final output SQL goes in here
-      
-      # Turn the name of the table into a class reference, and find the column defs
-      table_columns = @table_columns[TABLE_TO_CLASS_MAP[table_name]]
-      
-      options.each_pair do |key, value|
-        if table_columns.include?(key)
-          # If we have been given a key which corresponds to a column
-          if value.class == Array
-            # If it's an array, we substitute in a '?' in the statement for every element
-            where_query &lt;&lt; &quot;#{key} IN (&quot; + (1..value.length).map {'?'}.join(',') + &quot;)&quot;
-            where_args += value
-          else
-            where_query &lt;&lt; &quot;#{key} = ?&quot;
-            where_args &lt;&lt; value
-          end
-        end
-      end
-      
-      # Custom where clauses are specified with an array, with the first
-      # element being the statement to pass to prepare(), and the second being
-      # the arguments for that statement fragment.
-      #
-      # ie/  :where =&gt; [&quot;starttime BETWEEN ? AND ?&quot;, Time.now, Time.now + 3600]
-      if options.has_key?(:conditions) &amp;&amp; options[:conditions].class == Array
-        where_query &lt;&lt; options[:conditions].shift
-        where_args += options[:conditions] if options[:conditions].length &gt; 0
-      end
-      
-      # Assemble the fragments around WHERE and AND, if we need to
-      if where_query.length &gt; 0
-        assembled_query += &quot; WHERE #{where_query.join(&quot; AND &quot;)}&quot;
-      end
-      
-      # Allow specification of an ORDER column and direction
-      if options.has_key?(:order)
-        assembled_query += &quot; ORDER BY #{options[:order]}&quot;
-      end
+      # Set up a local logging object
+      @log = MythTV::Utils.setup_logging(options)
       
-      # Allow limiting of returned results
-      if options.has_key?(:limit)
-        assembled_query += &quot; LIMIT #{options[:limit]}&quot;
-      end
-
-      [assembled_query, where_args]
+      # Although we don't need to give back an instance, as we now work through
+      # class methods, we pass back self for the constructor
+      self
     end
-  
   end
 end
\ No newline at end of file</diff>
      <filename>lib/mythtv/database.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,31 +1,13 @@
 module MythTV
   
-  class Program
+  class Program &lt; ActiveRecord::Base
+    set_table_name 'program'
+    set_primary_keys 'chanid', 'starttime', 'manualid'
 
-    def initialize(program_array, db_instance)
-      # Find out the attributes this class has from the schema calculations earlier
-      columns = db_instance.table_columns[self.class]
-      
-      self.class.class_eval { attr_accessor(*columns) }
-      
-      @columns = columns
-
-      @columns.each_with_index do |col, i|
-        self.send(&quot;#{col}=&quot;, program_array[i])
-      end
-      
-      @db = db_instance
-    end
+    belongs_to :channel, :foreign_key =&gt; 'chanid'
     
-    def to_s; @columns.collect { |v| &quot;#{v}: '#{send(v) || 'nil'}'&quot; }.join(&quot;, &quot;); end
-  
-    def channel
-      channels = @db.list_channels(:conditions =&gt; ['chanid = ?', self.chanid])
-      if channels.length == 1
-        channels[0]
-      else
-        nil
-      end
+    def inspect
+      &quot;#{title} : #{subtitle} [#{category}] on at #{starttime} on channel id #{chanid}&quot;
     end
   end
   </diff>
      <filename>lib/mythtv/program.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,12 @@
 module MythTV
   
-  class RecordingSchedule
+  class RecordingSchedule &lt; ActiveRecord::Base
+    set_table_name 'record'
+    set_primary_key 'recordid'
+    set_inheritance_column nil
 
+    belongs_to :channel, :foreign_key =&gt; 'chanid', :class_name =&gt; &quot;MythTV::Channel&quot;
+    
     # Map the 'type' column to a string
     RS_TYPE_MAP = { 0 =&gt; :kNotRecording,
                     1 =&gt; :kSingleRecord,
@@ -30,139 +35,84 @@ module MythTV
                           :kDupCheckSubDesc     =&gt; 0x06,
                           :kDupCheckSubThenDesc =&gt; 0x08 }
     
-    
+    #
     COLUMN_TO_ENUM_MAP = { :type      =&gt; RS_TYPE_MAP,
                            :dupmethod =&gt; RS_DUPIN_MASK,
                            :dupmethod =&gt; RS_DUPMETHOD_MASK }
-
-    # Columns which need ENUMS: dupmethod, dupin, type? search?
-    # Foreign keys: transcoder, storagegroup
-    def initialize(data_source, db_instance)
-      @db = db_instance
-      
-      # Find out the attributes this class has from the schema calculations earlier
-      columns = db_instance.table_columns[self.class]
+    #
+    def initialize(options = nil)
+      super()
       
-      self.class.class_eval { attr_accessor(*columns) }
+      default_options = { :type =&gt; 6, # Find one of...
+                          :profile =&gt; 'Default',
+                          :recpriority =&gt; 0,
+                          :autoexpire =&gt;  MythTV::Setting.data_for_value('AutoExpireDefault'),
+                          :maxepisodes =&gt; 0,
+                          :maxnewest =&gt; 0,
+                          :startoffset =&gt; MythTV::Setting.data_for_value('DefaultStartOffset'),
+                          :endoffset =&gt; MythTV::Setting.data_for_value('DefaultEndOffset'),
+                          :recgroup =&gt; 'Default',
+                          :dupmethod =&gt; 6, # 
+                          :dupin =&gt; 15,
+                          :search =&gt; 0,
+                          :autotranscode =&gt; MythTV::Setting.data_for_value('AutoTranscode'),
+                          :autocommflag =&gt; MythTV::Setting.data_for_value('AutoCommercialFlag'),
+                          :autouserjob1 =&gt; MythTV::Setting.data_for_value('AutoRunUserJob1'),
+                          :autouserjob2 =&gt; MythTV::Setting.data_for_value('AutoRunUserJob2'),
+                          :autouserjob3 =&gt; MythTV::Setting.data_for_value('AutoRunUserJob3'),
+                          :autouserjob4 =&gt; MythTV::Setting.data_for_value('AutoRunUserJob4'),
+                          :findday =&gt; 0,
+                          :findtime =&gt; 0,
+                          :findid =&gt; 0,
+                          :inactive =&gt; 0,
+                          :parentid =&gt; 0,
+                          :transcoder =&gt; MythTV::Setting.data_for_value('DefaultTranscoder'),
+                          :tsdefault =&gt; 1,
+                          :playgroup =&gt; 'Default',
+                          :prefinput =&gt; 0,
+                          :next_record =&gt; Time.at(0),
+                          :last_record =&gt; Time.at(0),
+                          :last_delete =&gt; Time.at(0),
+                          :storagegroup =&gt; 'Default',
+                          :avg_delay =&gt; 0 }
       
-      @columns = columns
+      # TODO: The logic here isn't very clean. More DRY needed!
       
-      if data_source.class == Array
-        # If we're given an array, it's from the database, so construct via DATABASE_COLUMNS
-        @db.table_columns[RecordingSchedule].each_with_index do |col, i|
-          send(&quot;#{col}=&quot;, data_source[i])
-        end
-      elsif data_source.class == Program
-        # If we're initialising from a Program, invoke our new_from_program() method
-        new_from_program(data_source)
+      # We can be passed a Hash, or an instance of Program. In the Hash case, we
+      # merge the specified options in with the defaults before assignment happens
+      if options.is_a?(MythTV::Program) || options.nil?
+        program = options
+        merged_options = default_options
+      elsif options.class == Hash
+        program = nil
+        merged_options = default_options.merge(options)
       end
       
-    end
-    
-    # This method 
-    def new_from_program(program)
-      defaults = { :recordid =&gt; nil,
-                   :type =&gt; 1, # Single recording
-                   :profile =&gt; &quot;Default&quot;,
-                   :recpriority =&gt; 0,
-                   :autoexpire =&gt;  @db.get_setting('AutoExpireDefault'),
-                   :maxepisodes =&gt; 0,
-                   :maxnewest =&gt; 0,
-                   :startoffset =&gt; @db.get_setting('DefaultStartOffset'),
-                   :endoffset =&gt; @db.get_setting('DefaultEndOffset'),
-                   :recgroup =&gt; &quot;Default&quot;,
-                   :dupmethod =&gt; 6, # 
-                   :dupin =&gt; 15,
-                   :search =&gt; 0,
-                   :autotranscode =&gt; @db.get_setting('AutoTranscode'),
-                   :autocommflag =&gt; @db.get_setting('AutoTranscode'),
-                   :autouserjob1 =&gt; @db.get_setting('AutoRunUserJob1'),
-                   :autouserjob2 =&gt; @db.get_setting('AutoRunUserJob2'),
-                   :autouserjob3 =&gt; @db.get_setting('AutoRunUserJob3'),
-                   :autouserjob4 =&gt; @db.get_setting('AutoRunUserJob4'),
-                   :findday =&gt; 0,
-                   :findtime =&gt; 0,
-                   :findid =&gt; 0,
-                   :inactive =&gt; 0,
-                   :parentid =&gt; 0,
-                   :transcoder =&gt; @db.get_setting('DefaultTranscoder'),
-                   :tsdefault =&gt; 1,
-                   :playgroup =&gt; 'Default',
-                   :prefinput =&gt; 0,
-                   :next_record =&gt; 0,
-                   :last_record =&gt; 0,
-                   :last_delete =&gt; 0,
-                   :storagegroup =&gt; 'Default',
-                   :avg_delay =&gt; 0 }
-      
-      self.chanid = program.chanid
-      self.starttime = program.starttime
-      self.startdate = program.starttime
-      self.endtime = program.endtime
-      self.enddate = program.endtime
-      self.title = program.title
-      self.subtitle = program.subtitle
-      self.description = program.description
-      self.category = program.category
-      self.seriesid = program.seriesid
-      self.programid = program.programid
-
-      defaults.each_pair do |k,v|
-        self.send(&quot;#{k}=&quot;, v)
+      # Assign the options to self
+      merged_options.each_pair do |k, v|
+        self.send(&quot;#{k.to_s}=&quot;, v)
       end
       
-      # Station assignment from the channel object. Needs caching
-      channels = @db.list_channels(:chanid =&gt; self.chanid)
-      self.station = channels[0].name if channels.length == 1
-    end
-    
-    def save
-      query =  &quot;REPLACE INTO record (&quot; + @columns.collect { |c| c.to_s }.join(&quot;,&quot;) + &quot;)&quot;
-      query += &quot; VALUES (&quot; + (1..@columns.length).map {'?'}.join(',') + &quot;)&quot;
+      # If we've been passed a Program, we do some work to set up things up accordingly
+      if program
+        self.chanid = program.chanid
+        self.starttime = program.starttime
+        self.startdate = program.starttime
+        self.endtime = program.endtime
+        self.enddate = program.endtime
+        self.title = program.title
+        self.subtitle = program.subtitle
+        self.description = program.description
+        self.category = program.category
+        self.seriesid = program.seriesid
+        self.programid = program.programid
 
-      puts query
-      
-      st = @db.connection.prepare(query)
-      st_args = @columns.collect { |c| send(c) }
-      result = st.execute(*st_args)
-      
-      if result.affected_rows() == 1
-        # Set the recordid from the replace
-        @recordid = result.insert_id().to_s
-        return true
-      else
-        return false
-      end
-    end
-    
-    # Re-select all the data from the database via the primary key, recordid
-    def reload
-      st_query =  &quot;SELECT &quot; + @db.table_columns[self.class].collect { |c| c.to_s }.join(&quot;,&quot;) + &quot; FROM record&quot;
-      st_query += &quot; WHERE recordid = ?&quot;
-
-      st = @db.connection.prepare(st_query)
-      results = st.execute(@recordid)
-
-      @db.table_columns[self.class].each_with_index do |col, i|
-        send(&quot;#{col}=&quot;, result[i])
+        # Station assignment from the channel object. Needs caching?
+        channel = MythTV::Channel.find(self.chanid).name
       end
-    end
-    
-    # Remove the row from the database
-    def destroy
-      # We should have a valid recordid before we continue
-      return false if recordid.to_i &lt; 1
-      
-      st_query = &quot;DELETE FROM record WHERE recordid = ?&quot;
-      st = @db.connection.prepare(st_query)
-      result = st.execute(@recordid)
       
-      result.affected_rows() == 1
     end
-    
-    # Enable more pleasant debugging through a to_s method
-    def to_s; @columns.collect { |v| &quot;#{v}: '#{send(v) || 'nil'}'&quot; }.join(&quot;, &quot;); end
-    
+
   end
   
 end</diff>
      <filename>lib/mythtv/recording_schedule.rb</filename>
    </modified>
    <modified>
      <diff>@@ -45,4 +45,13 @@ module MythTV
     end
     
   end # end Utils
-end # end MythTV
\ No newline at end of file
+end # end MythTV
+
+# Workaround for a dependency within ActiveRecord on ActiveSupport multibyte strings
+# TODO: Will this be necessary going forward?
+module ActiveSupport
+  module Multibyte
+    class Chars
+    end
+  end
+end
\ No newline at end of file</diff>
      <filename>lib/mythtv/utils.rb</filename>
    </modified>
    <modified>
      <diff>@@ -21,7 +21,7 @@
 # THE SOFTWARE.
 
 module MythTV
-  VERSION = '0.2.0'
+  VERSION = '0.3.0'
   
   def self.connect(options)
     backend = connect_backend(options)
@@ -42,11 +42,12 @@ end
 
 $:.unshift(File.dirname(__FILE__))
 
+require 'mythtv/backend.rb'
+require 'mythtv/database.rb'
 require 'mythtv/channel.rb'
 require 'mythtv/program.rb'
 require 'mythtv/protocol.rb'
 require 'mythtv/recording.rb'
 require 'mythtv/recording_schedule.rb'
+require 'mythtv/setting.rb'
 require 'mythtv/utils.rb'
-require 'mythtv/database.rb'
-require 'mythtv/backend.rb'</diff>
      <filename>lib/ruby-mythtv.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,28 +1,27 @@
-Gem::Specification.new do |s|
+spec = Gem::Specification.new do |s|
   s.name = 'ruby-mythtv'
-  s.version = '0.2.0'
+  s.version = '0.3.0'
  
   s.specification_version = 2 if s.respond_to? :specification_version=
  
   s.required_rubygems_version = Gem::Requirement.new('&gt;= 0') if s.respond_to? :required_rubygems_version=
-  s.date = %q{2008-09-29}
-  s.description = %q{Ruby implementation of the MythTV communication protocol}
+  s.date = %q{2009-02-18}
+  s.description = %q{Ruby implementation of the MythTV communication protocol, and interface to the MythTV database}
   s.authors = [ 'Nick Ludlam' ]
   s.email = %q{nick@recoil.org}
-  s.extra_rdoc_files = [ 'History.txt', 'License.txt', 'README.txt' ]
-  s.files = [ 'History.txt', 'License.txt', 'README.txt', 'Rakefile',
-              'lib/ruby-mythtv.rb', 'mythtv/backend.rb', 'mythtv/channel.rb',
-              'mythtv/database.rb', 'mythtv/program.rb', 'mythtv/protocol.rb',
-              'mythtv/recording.rb', 'mythtv/recording_schedule.rb',
-              'mythtv/utils.rb', 'test/test_backend.rb', 'test/test_db.rb',
-              'test/test_helper.rb', 'test_stream.rb', 'Todo.txt' ]
+  s.extra_rdoc_files = [ 'History.txt', 'License.txt', 'README.txt', 'Todo.txt' ]
+  s.files = [ 'History.txt', 'License.txt', 'README.txt', 'Rakefile', 'Todo.txt' ] + Dir[&quot;lib/*.rb&quot;] + Dir[&quot;lib/mythtv/*.rb&quot;] + Dir[&quot;test/*.rb&quot;]
   s.has_rdoc = true
   s.homepage = %q{http://github.com/nickludlam/ruby-mythtv/}
   s.rdoc_options = ['--main', 'README.txt']
   s.require_paths = ['lib']
   s.rubyforge_project = %q{ruby-mythtv}
-  s.rubygems_version = %q{0.2.0}
+  s.rubygems_version = %q{0.3.0}
+  
   s.add_dependency('mysql')
+  s.add_dependency('activerecord')
+  s.add_dependency('composite_primary_keys')
+  
   s.summary = %q{Ruby implementation of the MythTV backend protocol}
-  s.test_files = [ 'test/test_helper.rb', 'test/test_backend.rb', 'test/test_db.rb' ]
-end
+  s.test_files = Dir[&quot;test/*.rb&quot;]
+end
\ No newline at end of file</diff>
      <filename>ruby-mythtv.gemspec</filename>
    </modified>
    <modified>
      <diff>@@ -2,8 +2,8 @@ require File.dirname(__FILE__) + '/test_helper.rb'
 
 class TestBackend &lt; Test::Unit::TestCase
   def setup
-    abort(&quot;\n\tERROR: You must set the environment variable MYTHTV_BACKEND to the name of your MythTV backend server\n\n&quot;) unless ENV['MYTHTV_BACKEND']
-    @backend = MythTV.connect_backend(:host =&gt; ENV['MYTHTV_BACKEND'])
+    abort(&quot;\n\tERROR: You must set the environment variable MYTHTV_HOST to the name of your MythTV backend server\n\n&quot;) unless ENV['MYTHTV_HOST']
+    @backend = MythTV.connect_backend(:host =&gt; ENV['MYTHTV_HOST'])
   end
   
   def teardown
@@ -45,6 +45,20 @@ class TestBackend &lt; Test::Unit::TestCase
     assert_equal test_image_sig, png_sig
   end
   
+  # In this test, we ignore the @backend instance connection which was created by setup()
+  # and perform our own block of commands on a new connection
+  def test_perform_commands
+    MythTV::Backend.perform_commands(:host =&gt; ENV['MYTHTV_HOST']) do |connection|
+      # Check the query_uptime method and query_memstats methods
+      uptime = connection.query_uptime
+      assert uptime &gt; 0
+      
+      memstats = connection.query_memstats
+      assert_equal Hash, memstats.class
+      assert_equal 4, memstats.keys.length
+    end
+  end
+  
   # def test_process_guide_xml
   #   guide_data = @backend.get_program_guide
   #   </diff>
      <filename>test/test_backend.rb</filename>
    </modified>
    <modified>
      <diff>@@ -2,103 +2,90 @@ require File.dirname(__FILE__) + '/test_helper.rb'
 
 class TestDatabase &lt; Test::Unit::TestCase
   def setup
-    abort(&quot;\n\tmyERROR: You must set the environment variable MYTHTV_DB to the name of your MythTV database server\n\n&quot;) unless ENV['MYTHTV_DB']
-    abort(&quot;\n\tmyERROR: You must set the environment variable MYTHTV_PW to the name of your MythTV database server\n\n&quot;) unless ENV['MYTHTV_PW']
-    @db = MythTV.connect_database(:host =&gt; ENV['MYTHTV_DB'],
-                                  :database_password =&gt; ENV['MYTHTV_PW'],
-                                  :log_level =&gt; Logger::DEBUG)
+    abort(&quot;\n\tERROR: You must set the environment variable MYTHTV_HOST to the name of your MythTV database server\n\n&quot;) unless ENV['MYTHTV_HOST']
+    abort(&quot;\n\tERROR: You must set the environment variable MYTHTV_PW to your MySQL MythTV password\n\n&quot;) unless ENV['MYTHTV_PW']
+    
+    conn_opts = Hash.new(:log_level =&gt; Logger::DEBUG)
+    conn_opts[:host] = ENV['MYTHTV_HOST'] if ENV.has_key?('MYTHTV_HOST')
+    conn_opts[:database_user] = ENV['MYTHTV_USER'] if ENV.has_key?('MYTHTV_USER')
+    conn_opts[:database_password] = ENV['MYTHTV_PW'] if ENV.has_key?('MYTHTV_PW')
+    
+    @db = MythTV::Database.new(conn_opts)
   end
   
   def teardown
-    @db.close
+    #@db.disconnect
   end
   
   # Check the DBSchemaVer key in the settings table as our first check
   # It should always be present
   def test_get_setting
-    schema_version = @db.get_setting('DBSchemaVer')
+    schema_version = MythTV::Setting.data_for_value('DBSchemaVer')
     assert schema_version.to_i &gt; 0
   end
 
-  # Check the DBSchemaVer, once queried, is in the setting cache
-  def test_get_setting_cache
-    schema_version = @db.get_setting('DBSchemaVer')
-    assert @db.setting_cache['DBSchemaVer_'].to_i &gt; 0
-  end
-
   def test_list_channels
-    channels = @db.list_channels
+    channels = MythTV::Channel.find(:all)
     
-    assert_kind_of Array, channels 
+    assert_kind_of Array, channels
     assert channels.length &gt; 0
-    assert_kind_of MythTV::Channel, channels[0] 
+    assert_kind_of MythTV::Channel, channels[0]
     assert channels[0].chanid &gt; 0
   end
   
   # Test we can pull back a single channel when
   # specifying a :chanid
   def test_list_single_chanid
-    first_channel_list = @db.list_channels
-    wanted_chanid = first_channel_list[0].chanid
+    channel_list = MythTV::Channel.find(:all)
+    wanted_chanid = channel_list[0].chanid
 
-    second_channel_list = @db.list_channels(:chanid =&gt; wanted_chanid)
-    assert_equal 1, second_channel_list.length
+    channel = MythTV::Channel.find_by_chanid(wanted_chanid)
 
-    channel = second_channel_list[0]
     assert_kind_of MythTV::Channel, channel
     assert_equal channel.chanid, wanted_chanid
   end
   
-  def test_list_multiple_chanids
-    first_channel_list = @db.list_channels
-    first_five = first_channel_list.slice(0..4)
-    wanted_chanids = first_five.map { |x| x.chanid }
-
-    second_channel_list = @db.list_channels(:chanid =&gt; wanted_chanids)
-    assert_equal 5, second_channel_list.length
-    second_channel_list
-  end
-  
   def test_list_programs
-    programs = @db.list_programs(:limit =&gt; 10)
+    programs = MythTV::Program.find(:all, :limit =&gt; 10)
     
     assert_equal 10, programs.length
   end
   
   def test_list_programs_with_search
-    programs = @db.list_programs(:conditions =&gt; ['title LIKE ?', &quot;%&quot;],
-                                 :limit =&gt; 5)
-    assert programs.length &gt; 0
+    programs = MythTV::Program.find(:all, :conditions =&gt; ['title like ?', '%'], :limit =&gt; 5)
+    assert_equal 5, programs.length
   end
   
   def test_list_programs_with_starttime_range
-    # Programs in the next two hours
-    programs = @db.list_programs(:conditions =&gt; ['starttime BETWEEN ? AND ?', Time.now, Time.now + 7200],
-                                 :limit =&gt; 1)
+    # Programs in the next two hours. Assume database is up to date
+    programs = MythTV::Program.find(:all, :conditions =&gt; ['starttime BETWEEN ? AND ?', Time.now, Time.now + 7200],
+                                          :limit =&gt; 1)
     
     assert_equal 1, programs.length
   end
   
   def test_program_links_to_channel
-    programs = @db.list_programs(:conditions =&gt; ['title LIKE ?', &quot;%&quot;], :limit =&gt; 1)
-    program_channel = programs[0].channel
-    assert_kind_of MythTV::Channel, program_channel
+    programs = MythTV::Program.find(:all, :limit =&gt; 1)
+    assert_kind_of MythTV::Channel, programs[0].channel
   end
 
   def test_new_schedule
     # Get list of schedules for later reference
-    num_schedules = @db.list_recording_schedules
-    programs = @db.list_programs(:conditions =&gt; ['starttime BETWEEN ? AND ?', Time.now + 3600, Time.now + 7200],
-                                 :limit =&gt; 1)
+    num_schedules = MythTV::RecordingSchedule.count
+    program = MythTV::Program.find(:first, :conditions =&gt; ['starttime BETWEEN ? AND ?', Time.now, Time.now + 7200],
+                                           :limit =&gt; 1)
+
+    # Check we have one at all
+    assert program
     
     # Convert our first program selected into a recording schedule
-    new_schedule = MythTV::RecordingSchedule.new(programs[0], @db)
+    new_schedule = MythTV::RecordingSchedule.new(program)
     new_schedule.save
     
     # Get new list
-    new_num_schedules = @db.list_recording_schedules
+    new_num_schedules = MythTV::RecordingSchedule.count
     # Assert that we now have one more schedule
-    assert_equal num_schedules.length + 1, new_num_schedules.length
+    assert_equal num_schedules + 1, new_num_schedules
     
     assert new_schedule.recordid.to_i &gt; 0
     
@@ -108,18 +95,22 @@ class TestDatabase &lt; Test::Unit::TestCase
   
   def test_new_and_modify_schedule
     # Get list of schedules for later reference
-    num_schedules = @db.list_recording_schedules
-    programs = @db.list_programs(:conditions =&gt; ['starttime BETWEEN ? AND ?', Time.now + 3600, Time.now + 7200],
-                                 :limit =&gt; 1)
+    num_schedules = MythTV::RecordingSchedule.count
+    program = MythTV::Program.find(:first, :conditions =&gt; ['starttime BETWEEN ? AND ?', Time.now, Time.now + 7200],
+                                           :limit =&gt; 1)
     
-    # Convert our first program selected into a recording schedule
-    new_schedule = MythTV::RecordingSchedule.new(programs[0], @db)
+    # Check we have one at all
+    assert program
+
+    # Convert our first program selected into a recording schedule, and save it
+    new_schedule = MythTV::RecordingSchedule.new(program)
     new_schedule.save
     
     new_schedule.type = 4
     new_schedule.save
 
-    test_query = @db.list_recording_schedules(:conditions =&gt; ['recordid = ?', new_schedule.recordid])
+    # We should find this now with new query
+    test_query = MythTV::RecordingSchedule.find(:all, :conditions =&gt; ['recordid = ?', new_schedule.recordid])
     assert_equal 1, test_query.length
     
     test_retrieval = test_query[0]</diff>
      <filename>test/test_db.rb</filename>
    </modified>
    <modified>
      <diff>@@ -12,23 +12,13 @@ class StreamFileHandler &lt; Mongrel::HttpHandler
 
   def process(request, response)
     pp &quot;request.params =&gt; #{request.params['REQUEST_PATH']}&quot;
-    
     response.write(&quot;HTTP/1.1 200 OK\r\n&quot;)
-    #response.write(&quot;Cache-Control: no-cache\r\n&quot;)
-    #response.write(&quot;Pragma: no-cache\r\n&quot;)
-    #response.write(&quot;ETag: \&quot;7531dc-102f000-44dda50e16281\&quot;\r\n&quot;)
     response.write(&quot;Accept-Ranges: bytes\r\n&quot;)
     response.write(&quot;Content-Length: 999999999\r\n&quot;)
     response.write(&quot;Keep-Alive: timeout=5, max=100\r\n&quot;)
     response.write(&quot;Connection: Keep-Alive\r\n&quot;)
     response.write(&quot;Content-type: video/mpeg\r\n\r\n&quot;)
     
-    # response.status = 200
-    # response.send_status(nil)
-    # response.header['Content-Type'] = &quot;video/mpeg&quot;
-    # response.header['Connection'] = &quot;Close&quot;
-    # response.send_header
-    
     buffer=&quot;&quot;
     total_bytes_read = 0
     </diff>
      <filename>test/test_stream.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>13783515b1e302e1bdedb1b08a08655c27885911</id>
    </parent>
  </parents>
  <author>
    <name>Nick Ludlam</name>
    <email>nick@Dusk.config</email>
  </author>
  <url>http://github.com/nickludlam/ruby-mythtv/commit/d42bde0bc2e75bf8466462b88989692a8d3e5cbc</url>
  <id>d42bde0bc2e75bf8466462b88989692a8d3e5cbc</id>
  <committed-date>2009-02-18T09:49:33-08:00</committed-date>
  <authored-date>2009-02-18T09:43:33-08:00</authored-date>
  <message>Deleted gemspec file</message>
  <tree>ae43f8c88a8c6750ea6540f3e64920841fc4352e</tree>
  <committer>
    <name>Nick Ludlam</name>
    <email>nick@Dusk.config</email>
  </committer>
</commit>
