Commits

Michael Granger committed ce6385a

Checkpoint commit

Comments (0)

Files changed (21)

 MONGREL2=/Users/mgranger/source/C/mongrel2/bin/mongrel2
-RUBY_OPT="-Ilib"
+RUBYOPT="-Ilib:../Strelka/lib:../Mongrel2/lib"
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+  s.name = "newznabr"
+  s.version = "0.0.1.20130629115103"
+
+  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+  s.authors = ["Michael Granger"]
+  s.cert_chain = ["/Users/mgranger/.gem/ged-public_gem_cert.pem"]
+  s.date = "2013-06-29"
+  s.description = "NewzNabr is a Ruby implementation of NewzNab, a usenet indexer."
+  s.email = ["ged@FaerieMUD.org"]
+  s.executables = ["newznabr"]
+  s.extra_rdoc_files = ["History.rdoc", "Manifest.txt", "README.rdoc", "History.rdoc", "README.rdoc"]
+  s.files = ["ChangeLog", "History.rdoc", "Manifest.txt", "Procfile", "README.rdoc", "Rakefile.rb", "bin/newznabr", "data/newznabr/apps/newznab-service", "data/newznabr/apps/newznabr-rest-service", "data/newznabr/apps/newznabr-webui", "data/newznabr/defaultcategories.yml", "data/newznabr/defaultgroups.yml", "data/newznabr/defaultregexes.yml", "data/newznabr/static/css/app.css", "data/newznabr/static/css/foundation.css", "data/newznabr/static/css/foundation.min.css", "data/newznabr/static/css/general_foundicons.css", "data/newznabr/static/css/loading.gif", "data/newznabr/static/css/normalize.css", "data/newznabr/static/fonts/general_foundicons.eot", "data/newznabr/static/fonts/general_foundicons.svg", "data/newznabr/static/fonts/general_foundicons.ttf", "data/newznabr/static/fonts/general_foundicons.woff", "data/newznabr/static/humans.txt", "data/newznabr/static/index.html", "data/newznabr/static/js/newznabr.js", "data/newznabr/static/js/vendor/angular-cookies.min.js", "data/newznabr/static/js/vendor/angular-loader.min.js", "data/newznabr/static/js/vendor/angular-mobile.min.js", "data/newznabr/static/js/vendor/angular-mocks.js", "data/newznabr/static/js/vendor/angular-resource.min.js", "data/newznabr/static/js/vendor/angular-sanitize.min.js", "data/newznabr/static/js/vendor/angular-scenario.js", "data/newznabr/static/js/vendor/angular.min.js", "data/newznabr/static/js/vendor/custom.modernizr.js", "data/newznabr/static/js/vendor/foundation.min.js", "data/newznabr/static/js/vendor/foundation/foundation.alerts.js", "data/newznabr/static/js/vendor/foundation/foundation.clearing.js", "data/newznabr/static/js/vendor/foundation/foundation.cookie.js", "data/newznabr/static/js/vendor/foundation/foundation.dropdown.js", "data/newznabr/static/js/vendor/foundation/foundation.forms.js", "data/newznabr/static/js/vendor/foundation/foundation.joyride.js", "data/newznabr/static/js/vendor/foundation/foundation.js", "data/newznabr/static/js/vendor/foundation/foundation.magellan.js", "data/newznabr/static/js/vendor/foundation/foundation.orbit.js", "data/newznabr/static/js/vendor/foundation/foundation.placeholder.js", "data/newznabr/static/js/vendor/foundation/foundation.reveal.js", "data/newznabr/static/js/vendor/foundation/foundation.section.js", "data/newznabr/static/js/vendor/foundation/foundation.tooltips.js", "data/newznabr/static/js/vendor/foundation/foundation.topbar.js", "data/newznabr/static/js/vendor/jquery-1.9.1.js", "data/newznabr/static/js/vendor/jquery-2.0.0.js", "data/newznabr/static/js/vendor/zepto.js", "data/newznabr/static/robots.txt", "data/newznabr/static/views/dashboard.html", "data/newznabr/static/views/navmenu.html", "data/newznabr/static/views/settings.html", "data/newznabr/templates/layout.tmpl", "data/newznabr/templates/top.tmpl", "etc/config.yml.sample", "etc/mongrel.rb", "lib/newznabr.rb", "lib/newznabr/binary.rb", "lib/newznabr/category.rb", "lib/newznabr/command.rb", "lib/newznabr/db.rb", "lib/newznabr/exceptions.rb", "lib/newznabr/group.rb", "lib/newznabr/mixins.rb", "lib/newznabr/model.rb", "lib/newznabr/nntpclient.rb", "lib/newznabr/nntpclient/group.rb", "lib/newznabr/nntpclient/response.rb", "lib/newznabr/part.rb", "lib/newznabr/partrepair.rb", "lib/newznabr/release.rb", "lib/newznabr/releasenfo.rb", "lib/newznabr/releaseregex.rb", "lib/sequel/plugins/inline_migrations.rb", "spec/data/caps.json", "spec/data/caps.xml", "spec/data/search.json", "spec/helpers.rb", "spec/newznabr/binary_spec.rb", "spec/newznabr/group_spec.rb", "spec/newznabr/nntpclient/group_spec.rb", "spec/newznabr/nntpclient_spec.rb", "spec/newznabr_spec.rb", "spec/sequel/plugins/inline_migrations_spec.rb", "spec/test-config.yml.example"]
+  s.homepage = "https://bitbucket.org/ged/newznabr"
+  s.rdoc_options = ["-f", "fivefish", "-t", "NewzNabr"]
+  s.require_paths = ["lib"]
+  s.required_ruby_version = Gem::Requirement.new("~> 2.0")
+  s.rubyforge_project = "newznabr"
+  s.rubygems_version = "2.0.3"
+  s.signing_key = "/Volumes/Keys/ged-private_gem_key.pem"
+  s.summary = "NewzNabr is a Ruby implementation of NewzNab, a usenet indexer."
+
+  if s.respond_to? :specification_version then
+    s.specification_version = 4
+
+    if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
+      s.add_runtime_dependency(%q<mail>, ["~> 2.5"])
+      s.add_runtime_dependency(%q<strelka>, ["~> 0.5"])
+      s.add_runtime_dependency(%q<sequel>, ["~> 3.48"])
+      s.add_runtime_dependency(%q<pg>, ["~> 0.15"])
+      s.add_runtime_dependency(%q<pluggability>, ["~> 0.2"])
+      s.add_runtime_dependency(%q<paint>, ["~> 0.8"])
+      s.add_runtime_dependency(%q<builder>, ["~> 3.2"])
+      s.add_runtime_dependency(%q<safe_yaml>, ["~> 0.9"])
+      s.add_runtime_dependency(%q<rdoc>, ["~> 4"])
+      s.add_development_dependency(%q<hoe-mercurial>, ["~> 1.4.0"])
+      s.add_development_dependency(%q<hoe-highline>, ["~> 0.1.0"])
+      s.add_development_dependency(%q<hoe-deveiate>, ["~> 0.2"])
+      s.add_development_dependency(%q<simplecov>, ["~> 0.7"])
+      s.add_development_dependency(%q<hoe>, ["~> 3.5"])
+    else
+      s.add_dependency(%q<mail>, ["~> 2.5"])
+      s.add_dependency(%q<strelka>, ["~> 0.5"])
+      s.add_dependency(%q<sequel>, ["~> 3.48"])
+      s.add_dependency(%q<pg>, ["~> 0.15"])
+      s.add_dependency(%q<pluggability>, ["~> 0.2"])
+      s.add_dependency(%q<paint>, ["~> 0.8"])
+      s.add_dependency(%q<builder>, ["~> 3.2"])
+      s.add_dependency(%q<safe_yaml>, ["~> 0.9"])
+      s.add_dependency(%q<rdoc>, ["~> 4"])
+      s.add_dependency(%q<hoe-mercurial>, ["~> 1.4.0"])
+      s.add_dependency(%q<hoe-highline>, ["~> 0.1.0"])
+      s.add_dependency(%q<hoe-deveiate>, ["~> 0.2"])
+      s.add_dependency(%q<simplecov>, ["~> 0.7"])
+      s.add_dependency(%q<hoe>, ["~> 3.5"])
+    end
+  else
+    s.add_dependency(%q<mail>, ["~> 2.5"])
+    s.add_dependency(%q<strelka>, ["~> 0.5"])
+    s.add_dependency(%q<sequel>, ["~> 3.48"])
+    s.add_dependency(%q<pg>, ["~> 0.15"])
+    s.add_dependency(%q<pluggability>, ["~> 0.2"])
+    s.add_dependency(%q<paint>, ["~> 0.8"])
+    s.add_dependency(%q<builder>, ["~> 3.2"])
+    s.add_dependency(%q<safe_yaml>, ["~> 0.9"])
+    s.add_dependency(%q<rdoc>, ["~> 4"])
+    s.add_dependency(%q<hoe-mercurial>, ["~> 1.4.0"])
+    s.add_dependency(%q<hoe-highline>, ["~> 0.1.0"])
+    s.add_dependency(%q<hoe-deveiate>, ["~> 0.2"])
+    s.add_dependency(%q<simplecov>, ["~> 0.7"])
+    s.add_dependency(%q<hoe>, ["~> 3.5"])
+  end
+end
-# .rvm.gems generated gem export file. Note that any env variable settings will be missing. Append these after using a ';' field separator
-rspec -v2.14.0.rc1 --pre
-hoe-deveiate -v0.2.0
-loggability -v0.6.0
-sequel -v3.45.0
+builder -v3.2.2
+gli -v2.8.0
+hoe-deveiate -v0.3.0
+mail -v2.5.4
+paint -v0.8.6
+pg -v0.17.0
 pluggability -v0.2.0
-simplecov -v0.7.1
-strelka -v0.5.0
-pg -v0.15.0
-
-paint -v0.8.5
-gli -v2.5.6
-
-builder -v3.2.0
-safe_yaml -v0.9.2
-
+safe_yaml -v0.9.7
+sequel -v4.2.0
+strelka -v0.6.0
 # Foreman Procfile
-webui: ruby -Ilib:../Strelka/lib -S strelka -l debug -c etc/config.yml start newznabr-webui
-rest: ruby -Ilib:../Strelka/lib -S strelka -l debug -c etc/config.yml start newznabr-rest-service
-api: ruby -Ilib:../Strelka/lib -S strelka -l debug -c etc/config.yml start newznab-service
+webui: ruby ../Strelka/bin/strelka -l debug -c etc/config.yml start newznabr-webui
+rest: ruby ../Strelka/bin/strelka -l debug -c etc/config.yml start newznabr-rest-service
+api: ruby ../Strelka/bin/strelka -l debug -c etc/config.yml start newznab-service
 mongrel: m2sh.rb -c etc/mongrel.sqlite start
 TEST_CONFIG = SPECDIR + 'test-config.yml'
 TEST_CONFIG_EXAMPLE = SPECDIR + 'test-config.yml.example'
 
+GEMSPEC = '.gemspec'
+
+
 $LOAD_PATH.unshift( 'lib' )
 
 
 # Sign gems
 Hoe.plugin :signing
 
-Hoe.spec 'newznabr' do
+$hoespec = Hoe.spec 'newznabr' do
 	self.readme_file = 'README.rdoc'
 	self.history_file = 'History.rdoc'
 	self.extra_rdoc_files = FileList[ '*.rdoc' ]
 
 	self.developer 'Michael Granger', 'ged@FaerieMUD.org'
 
+	self.dependency 'gli',              '~> 2.5'
 	self.dependency 'mail',             '~> 2.5'
 	self.dependency 'strelka',          '~> 0.5'
-	self.dependency 'sequel',           '~> 0.3'
+	self.dependency 'sequel',           '~> 4.2'
 	self.dependency 'pg',               '~> 0.15'
 	self.dependency 'pluggability',     '~> 0.2'
 	self.dependency 'paint',            '~> 0.8'
+	self.dependency 'builder',          '~> 3.2'
+	self.dependency 'safe_yaml',        '~> 0.9'
 
-	self.dependency 'hoe-deveiate',     '~> 0.2', :developer
+	self.dependency 'rdoc',             '~> 4'
+	self.dependency 'hoe-deveiate',     '~> 0.3', :developer
 	self.dependency 'simplecov',        '~> 0.7', :developer
 
 	self.require_ruby_version( '~> 2.0' )
 end
 
-
+# Fix some Hoe weirdness
+$hoespec.spec.files.delete( '.gemtest' )
 ENV['VERSION'] ||= $hoespec.spec.version.to_s
 
 # Ensure the specs pass before checking in
 task 'hg:precheckin' => [ :check_history, :check_manifest, :spec ]
 
 
+file GEMSPEC => __FILE__ do |task|
+	spec = $hoespec.spec
+	spec.version = "#{spec.version}.#{Time.now.strftime("%Y%m%d%H%M%S")}"
+	File.open( task.name, 'w' ) do |fh|
+		fh.write( spec.to_ruby )
+	end
+end
+
 desc "Build a coverage report"
 task :coverage do
 	ENV["COVERAGE"] = 'yes'
+{
+  "name": "newznabr",
+  "version": "0.0.1",
+  "main": "lib/newznabr.rb",
+  "ignore": [
+    "**/.*",
+    "node_modules",
+    "components",
+    "bower_components",
+    "test",
+    "tests"
+  ]
+}

data/newznabr/defaultregexes.yml

   values:
     :id: 10
     :groupname: alt.binaries.*
-    :regex: ^(?<name>[\w\.]+)-(?<from>\w+)(?<ext>\.\S+)?\s+-\s+"(?<filename>\S+)"\s+yEnc$
+    :regex: ^(?<name>[\w\.]+)-(?<fromname>\w+)(?<ext>\.\S+)?\s+-\s+"(?<filename>\S+)"\s+yEnc$
     :ordinal: 50
     :active: true
     :description: A better matcher
   values:
     :id: 11
     :groupname: alt.binaries.*
-    :regex: ^(?<name>[\w\.]+)-(?<from>\w+)(?<ext>\.\S+)?\s+-\s+\[(?<parts>\d+/\d+)\]\s+-\s+"(?<filename>\S+)"\s+yEnc$
+    :regex: ^(?<name>[\w\.]+)-(?<fromname>\w+)(?<ext>\.\S+)?\s+-\s+\[(?<parts>\d+/\d+)\]\s+-\s+"(?<filename>\S+)"\s+yEnc$
     :ordinal: 51
     :active: true
     :description: A better matcher with part numbers

data/newznabr/static/css/app.css

 }
 
 
+#dashboard-searchbox {
+	padding-top: 4em;
+}
+
+

data/newznabr/static/js/newznabr.js

 
 // Controllers
 angular.module('newznabr.controllers', ['newznabr.services']).
-	controller('DashboardController', ['$scope', 'Release', 'Binary',
-		function dashboardController($scope, Release, Binary) {
-			console.log( "Controller for Dashboard running." );
+	controller('DashboardController', 
+		function dashboardController($scope, $log, Release, Binary) {
+			$log.debug( "Controller for Dashboard running." );
 			$scope.viewname = 'Dashboard';
 			$scope.releases = Release.query({ limit: 15 });
+			$scope.searchterms = '';
+			$scope.search = function search(searchterms) {
+				$log.log( "Search for %o", searchterms );
+				$scope.releases = Release.query({ keywords: searchterms, limit: 15 });
+			};
 		}
-	]).
-	controller('SettingsController', ['$scope', '$log', 'Group', 'ReleaseRegexes',
+	).
+	controller('SettingsController',
 		function dashboardController($scope, $log, Group, ReleaseRegexes) {
-			console.log( "Controller for Settings running." );
+			$log.log( "Controller for Settings running." );
 			$scope.viewname = 'Settings';
 
 			Group.query(function(groups) {
 				});
 			};
 		}
-	]);
+	);
 
 
 // Filters
 	config(['$routeProvider', function($routeProvider) {
 		console.debug( "Setting up route provider." );
 		$routeProvider.
-			when('/dashboard', {
+			when('/', {
 				templateUrl: '/views/dashboard.html',
-				controller: 'DashboardController',
-				menuitem: 'Dashboard',
-				menuindex: 1
+				controller: 'DashboardController'
 			}).
 			when('/settings', {
 				templateUrl: '/views/settings.html',
 				styleclasses: ['right'],
 				menuindex: 99
 			}).
-			otherwise({ redirectTo: '/dashboard' });
+			otherwise({ redirectTo: '/' });
 	}]);
 

data/newznabr/static/views/dashboard.html

-<!-- First Band (Slider) -->
-
-<div class="row">
+<!-- Initial searchbox (no results) -->
+<div class="row" id="dashboard-searchbox" ng-hide="releases">
 	<div class="large-12 columns">
-		<form action="#/search" method="get" accept-charset="utf-8">
+		<form accept-charset="utf-8">
 			<fieldset class="row collapse">
 			<div class="large-10 columns">
-				<input type="text" placeholder="keywords, titles, etc.">
+				<input type="text" placeholder="keywords, titles, etc." ng-model="searchterms">
 			</div>
 			<div class="large-2 columns">
-				<input class="button prefix" type="submit" value="Search" />
+				<button ng-click="search(searchterms)" class="button prefix">Search</button>
 			</div>
 			</fieldset>
 		</form>
 	</div>
 </div>
   
-<!-- Three-up Content Blocks -->
+<!-- Searchbox+Results -->
+<div class="row" ng-show="releases">
+	<div class="large-12 columns">
 
-<div class="row">
+		<form accept-charset="utf-8">
+			<fieldset class="row collapse">
+			<div class="large-10 columns">
+				<input type="text" placeholder="keywords, titles, etc." ng-model="searchterms">
+			</div>
+			<div class="large-2 columns">
+				<button ng-click="search(searchterms)" class="button prefix">Search</button>
+			</div>
+			</fieldset>
+		</form>
 
-	<div class="large-12 columns">
-	<table id="release-results">
-		<thead>
-			<tr>
-			  <th>Name</th>
-			  <th>From</th>
-			  <th>Date</th>
-			  <th>Parts</th>
-			</tr>
-		</thead>
-		<tbody>
-			<tr ng-repeat="binary in binaries">
-			  <td>{{binary.name}}</td>
-			  <td>{{binary.fromname}}</td>
-			  <td>{{binary.date | reldate}}</td>
-			  <td>{{binary.total_parts}}</td>
-			</tr>
-		</tbody>
-	</table>
+		<table id="release-results">
+			<thead>
+				<tr>
+				  <th>Name</th>
+				  <th>From</th>
+				  <th>Date</th>
+				  <th>Parts</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="release in releases">
+				  <td>{{release.name}}</td>
+				  <td>{{release.fromname}}</td>
+				  <td>{{release.date | reldate}}</td>
+				  <td>{{release.total_parts}}</td>
+				</tr>
+			</tbody>
+		</table>
 	
 	</div>
-    	
 </div>
     
-<!-- Call to Action Panel -->
-<div class="row">
-	<div class="large-12 columns">
-    
-		<div class="panel">
-			<h4>Your Request</h4>
-            
-			<div class="row">
-				<div class="large-9 columns">
-					<p>Your request looked this:</p>
-
-					<pre><code>
-						<?pp request ?>
-					</code></pre>
-				</div>
-				<div class="large-3 columns">
-					<a href="#" class="radius button right">Contact Us</a>
-				</div>
-			</div>
-		</div>
-      
-	</div>
-</div>
-
 <!-- Footer -->
-  
 <footer class="row">
 	<div class="large-12 columns">
 		<hr />
 		<div class="row">
 			<div class="large-6 columns">
-				<p>&copy; Copyright no one at all. Go to town.</p>
 			</div>
 			<div class="large-6 columns">
 				<ul class="inline-list right">
-					<li><a href="#">Link 1</a></li>
-					<li><a href="#">Link 2</a></li>
-					<li><a href="#">Link 3</a></li>
-					<li><a href="#">Link 4</a></li>
+					<li><a href="https://bitbucket.org/ged/newznabr">Project</a></li>
+					<li><a href="http://newznab.readthedocs.org/en/latest/">Newznab Info</a></li>
 				</ul>
 			</div>
 		</div>

data/newznabr/static/views/settings.html

 								</td>
 							</tr>
 							<tr>
-								<td colspan="3">{{re.regex}}</td>
+								<td colspan="3"><code>{{re.regex}}</code></td>
 							</tr>
 						</tbody>
 					</table>
 server 'main' do
 	name 'newznabr'
 
-	chroot '.'
+	chroot ''
 
-	access_log '/logs/access.log'
-	error_log  '/logs/error.log'
-	pid_file '/run/mongrel2.pid'
+	access_log 'logs/access.log'
+	error_log  'logs/error.log'
+	pid_file 'run/mongrel2.pid'
 
 	bind_addr '0.0.0.0'
 	port 8223
 			route "/#{subdir.basename}/(.)", directory( subdir.to_s + '/' )
 		end
 
-		route '/favicon.ico($)', directory( datadir + 'static/img/', default: 'favicon.ico' )
+		route '/favicon.ico($)', directory( datadir + 'static/img/', 'favicon.ico' )
 
 		route '/services/newznabr/v1', handler( 'tcp://127.0.0.1:29001', 'newznabr-rest-service' )
 		route '/api', handler( 'tcp://127.0.0.1:29003', 'newznab-service' )
 end
 
 setting 'limits.content_length', '5242880'
+setting 'server.daemonize', false
 
 mkdir_p 'logs'
 mkdir_p 'run'

lib/newznabr/binary.rb

 		$
 	:x
 
+	# The possible states of the 'procstat' field
+	PROCSTAT_STATES = %i[
+		new
+		titlematched
+		ready
+		wrongparts
+		released
+		duplicate
+		reqidlookupfailed
+	]
+
+	### Override table creation to add the associated enum type.
+	def self::create_table( * )
+		self.db << "DROP TYPE IF EXISTS binary_procstat"
+		self.db << "CREATE TYPE binary_procstat AS ENUM (" +
+			PROCSTAT_STATES.map {|state| "'#{state}'" }.join(', ') +
+			")"
+
+		super
+	end
+
+
+	### Override table dropping to also drop the enum type.
+	def self::drop_table( * )
+		super
+		self.db << "DROP TYPE IF EXISTS binary_procstat"
+	end
+
 
 	# Define the schema. If you're changing this, you should also be defining
 	# a migration.
 	set_schema do
 		bigserial :id, primary_key: true
 
-		text      :binaryhash, unique: true
+		text            :binaryhash, unique: true
 
-		text      :name,          null: false, default: ''
-		text      :fromname,      null: false, default: ''
-		timestamp :date
-		text      :xref,          null: false, default: ''
-		int       :total_parts,   null: false, default: 0
-		int       :procstat,      default: 0
-		int       :procatttempts, default: 0
-		int       :category
-		int       :reqid
-		int       :relpart,       default: 0
-		int       :reltotalpart,  default: 0
-		text      :relname
-		text      :importname
-		timestamp :date_added, default: Sequel.function(:now)
+		text            :name,          null: false, default: ''
+		text            :fromname,      null: false, default: ''
+		timestamp       :date
+		text            :xref,          null: false, default: ''
+		int             :total_parts,   null: false, default: 0
+		binary_procstat :procstat,      default: 'new'
+		int             :procatttempts, default: 0
+		int             :category
+		int             :reqid
+		int             :relpart,       default: 0
+		int             :reltotalpart,  default: 0
+		text            :relname
+		text            :importname
+		timestamp       :date_added,    default: Sequel.function(:now)
 
 		foreign_key :regex_id, :newznabr__release_regexes,
 			on_delete: :set_null
 
 
 	#
+	# Subsets
+	#
+
+	##
+	# Look up a release by its GUID.
+	subset( :unprocessed, procstat: 'new' )
+	subset( :titlematched, procstat: 'titlematched' )
+	subset( :ready, procstat: 'ready' )
+	subset( :wrongparts, procstat: 'wrongparts' )
+	subset( :released, procstat: 'released' )
+	subset( :duplicate, procstat: 'duplicate' )
+	subset( :reqidlookupfailed, procstat: 'reqidlookupfailed' )
+
+	# SELECT *
+	# FROM binaries AS b
+	# WHERE total_parts = (
+	#     SELECT COUNT(*) FROM parts WHERE binary_id = b.id
+	# );
+	subset( :completed ) do
+		where( total_parts: NewzNabr::Part.where(binary_id: "b.id").count )
+	end
+
+	#
 	# Relations
 	#
 

lib/newznabr/command.rb

 		NewzNabr::ReleaseRegex.each do |rre|
 			$stderr.puts "Looking at RRE: %s" % [ rre.regex ]
 			rre.matching_binaries.each do |binary, match|
+				self.log.debug "  match: %p" % [ match ]
 				named_captures = Hash[ [match.names, match.captures].transpose ]
-				puts "Matched: %p" % [ named_captures ]
+				rel = NewzNabr::Release.from_regexp_match( rre, named_captures )
+				self.prompt.say "  %s" % [ rel ]
 			end
 		end
 	end

lib/newznabr/db.rb

 		self.connection.transaction do
 			datadir = NewzNabr::DATA_DIR
 			datafile = datadir + filename
-			objects = YAML.load_file( datafile ) or
+			objects = YAML.load_file( datafile, safe: false ) or
 				raise "Failed to load objects from %p." % [ datafile ]
 
 			modelclass = objects.first.class

lib/newznabr/group.rb

 		overviews = nntpclient.overview( self.unread_article_range )
 
 		last_article_number = nil
-		binaries = {}
 
 		overviews.each do |overview|
 			binary = NewzNabr::Binary.from_overview( overview, self ) or next
-			binaries[ binary.name ] = binary
 			last_article_number = overview[:article_number]
 			self.log.info "  article %d (%s)..." % [ last_article_number, binary.date ] if
 				(last_article_number % 1000).zero?
 		end
 
-		return binaries
 	ensure
 		if last_article_number
 			self.log.info "  scanned up to article %d of %s" % [ self.last_article, self.name ]

lib/newznabr/part.rb

 		foreign_key :binary_id, :newznabr__binaries,
 			on_delete: :cascade,
 			on_update: :cascade
+
+		index :binary_id
 	end
 
 end # class NewzNabr::Part

lib/newznabr/release.rb

 
 class NewzNabr::Release < NewzNabr::Model( :newznabr__releases )
 
+	# The columns which should be represented by regexp capture groups
+	REGEXP_CAPTURE_GROUPS = %w[name fromname]
+
+
 	# Define the schema. If you're changing this, you should also be defining
 	# a migration.
 	set_schema do
 	many_to_one :category, class_name: 'NewzNabr::Category'
 
 
+	### Find an existing release matched by the specified +release_regex+ with the
+	### given +captures+, or create a new one.
+	def self::from_regexp_match( release_regex, captures )
+		self.find_or_create( name: captures[:name], fromname: captures[:fromname] ) do |release|
+			release.name     = captures[:name]
+			release.fromname = captures[:fromname]
+			release.regex    = release_regex
+			require 'pry'; binding.pry
+		end
+	end
+
 
 end # class NewzNabr::Release
 

lib/newznabr/releaseregex.rb

 		end
 	end
 
+
+	### Validate the release regex before it's inserted/updated.
+	def validate
+		self.validate_regex
+		self.validate_ordinal
+	end
+
+
+	### Validate that the pattern associated with the receiver is valid and contains the
+	### required capture groups.
+	def validate_regex
+		re = nil
+		begin
+			re = Regexp.compile( self[:regex] )
+		rescue RegexpError => err
+			return self.errors.add( :regex, "Invalid regexp: #{err.message}" )
+		end
+
+		NewzNabr::Release::REGEXP_CAPTURE_GROUPS.each do |name|
+			unless re.names.include?( name )
+				return self.errors.add( :regex, "missing required capture group #{name.dump}" )
+			end
+		end
+	end
+
+
+	### Ensure the ordinal field is valid.
+	def validate_ordinal
+		self.validates_integer( :ordinal )
+	end
+
 end # class NewzNabr::ReleaseRegex
 
 # encoding: utf-8
 # vim: set nosta noet ts=4 sw=4:
 
+$LOAD_PATH.unshift '/Users/ged/source/ruby/Mongrel2/lib'
+
 # SimpleCov test coverage reporting; enable this using the :coverage rake task
 if ENV['COVERAGE']
 	$stderr.puts "\n\n>>> Enabling coverage report.\n\n"
 
 require 'pathname'
 require 'time'
+require 'mongrel2'
 require 'strelka'
 
 require 'mongrel2/testing'

spec/newznabr/releaseregex_spec.rb

+#!/usr/bin/env rspec -cfd -b
+
+require_relative '../helpers'
+
+require 'rspec'
+require 'newznabr'
+
+describe 'NewzNabr::ReleaseRegex' do
+
+	before( :all ) do
+		setup_logging()
+		setup_test_database()
+	end
+
+	after( :all ) do
+		cleanup_test_database()
+		reset_logging()
+	end
+
+
+	let( :described_class ) { NewzNabr::ReleaseRegex }
+	let( :acceptable_regex ) do
+		NewzNabr::Release::REGEXP_CAPTURE_GROUPS.map {|cap| "(?<#{cap}>.)" }.join
+	end
+
+
+	it "rejects invalid regexes" do
+		obj = described_class.new( regex: '*', ordinal: 55 )
+		expect( obj ).not_to be_valid
+		expect( obj.errors ).to include( :regex )
+		expect( obj.errors[:regex].first ).to match( /invalid regexp/i )
+	end
+
+
+	it "rejects patterns that don't have all expected capture groups" do
+		obj = described_class.new( regex: '.*', ordinal: 55 )
+		expect( obj ).not_to be_valid
+		expect( obj.errors ).to include( :regex )
+		expect( obj.errors[:regex].first ).to match( /missing required capture group/i )
+	end
+
+
+	it "rejects instances with a missing ordinal" do
+		obj = described_class.new( regex: acceptable_regex )
+		expect( obj ).not_to be_valid
+		expect( obj.errors ).to include( :ordinal )
+		expect( obj.errors[:ordinal].first ).to match( /is not a number/i )
+	end
+
+
+	it "accepts an instance with valid regex and ordinal" do
+		obj = described_class.new( regex: acceptable_regex, ordinal: 55 )
+		expect( obj ).to be_valid
+	end
+
+
+	it "returns its regex as a Regexp object" do
+		obj = described_class.new( regex: acceptable_regex )
+		expect( obj.regex ).to eq( Regexp.compile(acceptable_regex) )
+	end
+
+
+	it "allows its regex to be set to a Regexp" do
+		obj = described_class.new
+		obj.regex = /a testing regex/
+		expect( obj[:regex] ).to eq( "a testing regex" )
+	end
+
+
+	it "can make a Regexp out of its groupname pattern" do
+		obj = described_class.new( groupname: 'news.example.*' )
+		expect( obj.groupname_regex ).to eq( /news\.example\..*/ )
+	end
+
+
+end