#! /usr/bin/ruby # # = amc.rb - Ant Movie Catalog files parser and generator # # Copyright (c) 2008 Daniel Rodríguez Troitiño # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'stringio' require 'date' class Hash # Creates a new hash containing only the keys included as parameters. def pass(*keys) self.delete_if { |k, v| !keys.include?(k) } end end module IOExtensions # Reads a 4 byte little-endian integer from the stream. def read_integer self.read(4).unpack('V')[0] end # Writes a 4 byte little-endian to the stream. def write_integer(n) self.write([n].pack('V')) end START_DATE = Date.new(1899, 12, 30) # First day for Borland TDateTime. # Reads a TDateTime (4 bytes) from the stream. def read_date d = self.read_integer START_DATE + d end # Writes a TDateTime (4 bytes) to the stream. def write_date(date) d = date - START_DATE self.write_integer(d) end # Reads a boolean (1 byte, non-zero means true) from the stream. def read_boolean (self.read(1).unpack('C')[0] & 0xFF) != 0x00 end # Writes a boolean (1 byte) to the stream. def write_boolean(b) self.write(b ? "\x01" : "\x00") end # Reads a string (length in 4 bytes and the string) from a stream. def read_pascal_string size = self.read_integer if size > 0 self.read(size) else '' end end # Writes a string to a stream. def write_pascal_string(str) size = str.size self.write_integer(size) self.write(str) end end class IO #:nodoc: include IOExtensions end class StringIO #:nodoc: include IOExtensions end module AMC class AMCError < StandardError #:nodoc: end class InvalidFileFormat < AMCError #:nodoc: end # A movie entry from a Ant Movie Catalog. Stores all information about the # movie, included its picture data if present. # # Movie defines methods for getting and setting all the movie properties. class Movie # Possible keys in a movie entry. VALID_KEYS = { :number => 1, :date => DateTime.now, :rating => 0, :year => DateTime.now.year, :length => 0, :video_bitrate => 0, :audio_bitrate => 0, :disks => 0, :checked => true, :media => '', :media_type => '', :source => '', :borrower => '', :original_title => '', :translated_title => '', :director => '', :producer => '', :country => '', :category => '', :actors => '', :url => '', :description => '', :comments => '', :video_format => '', :audio_format => '', :resolution => '', :framerate => '', :languages => '', :subtitles => '', :size => '', :picture_name => '', :picture_size => 0, :picture => '' } # Creates a new movie from the hash data. def initialize(data = {}) @data = data.empty? VALID_KEYS.clone : data.pass(*VALID_KEYS.keys) end VALID_KEYS.keys.each do |k| define_method(k) do @data[k] end define_method((k.to_s + '=').to_sym) do |v| @data[k] = v end end # Creates a new movie from the data stored in a stream. The stream is # suppose to be positioned at the start of the movie entry data. The # stream is left after the last data byte of the movie entry. def self.from_stream(stream) m = Movie.new m.number = stream.read_integer m.date = stream.read_date [:rating, :year, :length, :video_bitrate, :audio_bitrate, :disks].each do |k| m.send(k.to_s + '=', stream.read_integer) end m.checked = stream.read_boolean [:media, :media_type, :source, :borrower, :original_title, :translated_title, :director, :producer, :country, :category, :actors, :url, :description, :comments, :video_format, :audio_format, :resolution, :framerate, :languages, :subtitles, :size].each do |k| m.send(k.to_s + '=', stream.read_pascal_string) end m.picture_name = stream.read_pascal_string m.picture_size = stream.read_integer m.picture = stream.read(m.picture_size) return m end # Saves a movie in binary form. def serialize s = ::StringIO.new s.write_integer(self.number) s.write_date(self.date) [:rating, :year, :length, :video_bitrate, :audio_bitrate, :disks].each do |k| s.write_integer(self.send(k)) end s.write_boolean(self.checked) [:media, :media_type, :source, :borrower, :original_title, :translated_title, :director, :producer, :country, :category, :actors, :url, :description, :comments, :video_format, :audio_format, :resolution, :framerate, :languages, :subtitles, :size].each do |k| s.write_pascal_string(self.send(k)) end s.write_pascal_string(self.picture_name) s.write_integer(self.picture_size) s.write(self.picture) return s.string end end # A whole catalog from Ant Movie Catalog. Stores the metainformation about # the user and the movies listed in the catalog. # # Catalog defines methods for getting and setting all the catalog # properties and accessing the movie array. class Catalog # Possible keys in a catalog. VALID_KEYS = { :owner_name => '', :owner_site => '', :owner_mail => '', :owner_description => '' } # File header form Ant Movie Catalog, version 3.5.x STR_FILE_HEADER_35 = " AMC_3.5 Ant Movie Catalog 3.5.x www.buypin.com www.antp.be " # Creates a new catalog with the data provided in the hash a no movies. def initialize(data = VALID_KEYS) @data = data.pass(*VALID_KEYS.keys) @movies = [] end VALID_KEYS.keys.each do |k| define_method(k) do @data[k] || '' end define_method((k.to_s + '=').to_sym) do |v| @data[k] = v end end attr_accessor :movies # Create a new catalog from the data contained in the specified file. def self.from_file(filename) open(filename, 'rb') do |f| c = Catalog.new raise InvalidFileFormat.new if f.read(65) != STR_FILE_HEADER_35 VALID_KEYS.keys.each do |k| c.send(k.to_s + '=', f.read_pascal_string) end until f.eof? c.movies << Movie.from_stream(f) end return c end end # Save the catalog to a file. def save(filename) open(filename, 'wb') do |f| f.write(STR_FILE_HEADER_35) VALID_KEYS.keys.each do |k| f.write_pascal_string(self.send(k)) end self.movies.each do |m| f.write(m.serialize) end end end end end if __FILE__ == $0 c = AMC::Catalog.from_file(ARGV[0]) c.movies.each do |m| puts "#{m.original_title} (#{m.year})" end end