<commit_before>/*
 * PhotoStore.cs
 *
 * Author(s):
 *	<NAME> <<EMAIL>>
 *	<NAME> <<EMAIL>>
 *	<NAME> <<EMAIL>>
 * 
 * This is free software. See COPYING for details.
 */

using Gnome;
using Gnome.Vfs;
using Gdk;
using Gtk;

using Mono.Data.SqliteClient;
using Mono.Unix;

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System;

using FSpot;
using FSpot.Query;
using FSpot.Utils;

using Banshee.Database;


public class PhotoStore : DbStore {
	public int TotalPhotos {
		get {
			SqliteDataReader reader = Database.Query("SELECT COUNT(*) AS photo_count FROM photos");
			reader.Read ();
			int total = Convert.ToInt32 (reader ["photo_count"]);
			reader.Close ();
			return total;
		}
	}

	public static ThumbnailFactory ThumbnailFactory = new ThumbnailFactory (ThumbnailSize.Large);

	// FIXME this is a hack.  Since we don't have Gnome.ThumbnailFactory.SaveThumbnail() in
	// GTK#, and generate them by ourselves directly with Gdk.Pixbuf, we have to make sure here
	// that the "large" thumbnail directory exists.
	private static void EnsureThumbnailDirectory ()
	{
		string large_thumbnail_file_name_template = Thumbnail.PathForUri ("file:///boo", ThumbnailSize.Large);
		string large_thumbnail_directory_path = System.IO.Path.GetDirectoryName (large_thumbnail_file_name_template);

		if (! System.IO.File.Exists (large_thumbnail_directory_path))
			System.IO.Directory.CreateDirectory (large_thumbnail_directory_path);
	}

	//
	// Generates the thumbnail, returns the Pixbuf, and also stores it as a side effect
	//

	public static Pixbuf GenerateThumbnail (System.Uri uri)
	{
		using (FSpot.ImageFile img = FSpot.ImageFile.Create (uri)) {
			return GenerateThumbnail (uri, img);
		}
	}

	public static Pixbuf GenerateThumbnail (System.Uri uri, ImageFile img)
	{
		Pixbuf thumbnail = null;

		if (img is FSpot.IThumbnailContainer) {
			try {
				thumbnail = ((FSpot.IThumbnailContainer)img).GetEmbeddedThumbnail ();
			} catch (Exception e) {
				Log.DebugFormat ("Exception while loading embedded thumbail {0}", e.ToString ());
			}
		}

		// Save embedded thumbnails in a silightly invalid way so that we know to regnerate them later.
		if (thumbnail != null) {
			PixbufUtils.SaveAtomic (thumbnail, FSpot.ThumbnailGenerator.ThumbnailPath (uri), 
						"png", new string [] { null} , new string [] { null});
			//FIXME with gio, set it to uri time minus a few sec
			System.IO.File.SetLastWriteTime (FSpot.ThumbnailGenerator.ThumbnailPath (uri), new DateTime (1980, 1, 1));
		} else 
			thumbnail = FSpot.ThumbnailGenerator.Create (uri);
		
		return thumbnail;
	}

	public static void DeleteThumbnail (System.Uri uri)
	{
		string path = Thumbnail.PathForUri (uri.ToString (), ThumbnailSize.Large);
		if (System.IO.File.Exists (path))
			System.IO.File.Delete (path);
	}

	public static void MoveThumbnail (string old_path, string new_path)
	{
		System.IO.File.Move (ThumbnailGenerator.ThumbnailPath (UriUtils.PathToFileUri (old_path)),
			   ThumbnailGenerator.ThumbnailPath(UriUtils.PathToFileUri (new_path)));
	}

	// Constructor

	public PhotoStore (QueuedSqliteDatabase database, bool is_new)
		: base (database, false)
	{
		EnsureThumbnailDirectory ();

		if (! is_new)
			return;
		
		Database.ExecuteNonQuery ( 
			"CREATE TABLE photos (\n" +
			"	id			INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \n" +
			"	time			INTEGER NOT NULL, \n" +
			"	uri			STRING NOT NULL, \n" +
			"	description		TEXT NOT NULL, \n" +
			"	roll_id			INTEGER NOT NULL, \n" +
			"	default_version_id	INTEGER NOT NULL, \n" +
			"	rating			INTEGER NULL, \n" +
			"	md5_sum			TEXT NULL\n" +
			")");

		Database.ExecuteNonQuery (
			"CREATE TABLE photo_tags (\n" +
			"	photo_id	INTEGER, \n" +
			"       tag_id		INTEGER, \n" +
			"       UNIQUE (photo_id, tag_id)\n" +
			")");

		Database.ExecuteNonQuery (
			"CREATE TABLE photo_versions (\n"+
			"	photo_id	INTEGER, \n" +
			"	version_id	INTEGER, \n" +
			"	name		STRING, \n" +
			"	uri		STRING NOT NULL, \n" +
			"	md5_sum		STRING NOT NULL, \n" +
			"	protected	BOOLEAN, \n" +
			"	UNIQUE (photo_id, version_id)\n" +
			")");

		Database.ExecuteNonQuery ("CREATE INDEX idx_photo_versions_id ON photo_versions(photo_id)");
		Database.ExecuteNonQuery ("CREATE INDEX idx_photos_roll_id ON photos(roll_id)");
	}

	public Photo CheckForDuplicate (System.Uri uri) {
		// Here we can go wild in comparing photos,
		// for now we check on uri and md5
		Photo found = GetByUri (uri);
		
		if (found != null)
		 	return found;

		string md5 = Photo.GenerateMD5 (uri);			
		Gnome.Vfs.FileInfo info = new Gnome.Vfs.FileInfo (uri.ToString (), FileInfoOptions.GetMimeType);

		Photo[] md5_matches = GetByMD5 (md5);

		foreach (Photo match in md5_matches)
		{
		 	Gnome.Vfs.FileInfo match_info = new Gnome.Vfs.FileInfo (match.DefaultVersionUri.ToString (), FileInfoOptions.GetMimeType);

			// same mimetype?
			if (info.MimeType != match_info.MimeType)
			 	continue;

			// other comparisons?

			// TODO? load pixbuf and compare sizes?	

			return match;
		}

		return null;
	}

	[Obsolete ("Use Create (Uri, uint, out Pixbuf) instead")]
	public Photo Create (string path, uint roll_id, out Pixbuf thumbnail)
	{
		return Create (path, path, roll_id, out thumbnail);
	}

	[Obsolete ("Use Create (Uri, Uri, uint, out Pixbuf) instead")]
	public Photo Create (string new_path, string orig_path, uint roll_id, out Pixbuf thumbnail)
	{
		return Create (UriUtils.PathToFileUri (new_path), UriUtils.PathToFileUri (orig_path), roll_id, out thumbnail);
	}

	public Photo Create (System.Uri uri, uint roll_id, out Pixbuf thumbnail)
	{
		return Create (uri, uri, roll_id, out thumbnail);
	}

	public Photo Create (System.Uri new_uri, System.Uri orig_uri, uint roll_id, out Pixbuf thumbnail)
	{
		Photo photo;
		using (FSpot.ImageFile img = FSpot.ImageFile.Create (orig_uri)) {
			long unix_time = DbUtils.UnixTimeFromDateTime (img.Date);
			string description = img.Description != null  ? img.Description.Split ('\0') [0] : String.Empty;
			string md5_sum = Photo.GenerateMD5 (new_uri);

	 		uint id = (uint) Database.Execute (
				new DbCommand (
					"INSERT INTO photos (time, uri, description, roll_id, default_version_id, rating, md5_sum) "	+
	 				"VALUES (:time, :uri, :description, :roll_id, :default_version_id, :rating, :md5_sum)",
	 				"time", unix_time,
					"uri", new_uri.OriginalString,
	 				"description", description,
					"roll_id", roll_id,
	 				"default_version_id", Photo.OriginalVersionId,
					"rating", "0",
					"md5_sum", md5_sum
				)
			);
	
			photo = new Photo (id, unix_time, new_uri, md5_sum);
			AddToCache (photo);
			photo.Loaded = true;
	
			thumbnail = GenerateThumbnail (new_uri, img);		
			EmitAdded (photo);
		}
		return photo;
	}


	private void GetVersions (Photo photo)
	{
		SqliteDataReader reader = Database.Query(
			new DbCommand("SELECT version_id, name, uri, md5_sum, protected " + 
				      "FROM photo_versions " + 
				      "WHERE photo_id = :id", 
				      "id", photo.Id
			)
		);

		while (reader.Read ()) {
			uint version_id = Convert.ToUInt32 (reader ["version_id"]);
			string name = reader["name"].ToString ();
#if MONO_2_0
			System.Uri uri = new System.Uri (reader["uri"].ToString ());
#else
			System.Uri uri = new System.Uri (reader["uri"].ToString (), true);
#endif
			string md5_sum = reader["md5_sum"].ToString ();
			bool is_protected = Convert.ToBoolean (reader["protected"]);
			photo.AddVersionUnsafely (version_id, uri, md5_sum, name, is_protected);
		}
		reader.Close();
	}

	private void GetTags (Photo photo)
	{
		SqliteDataReader reader = Database.Query(new DbCommand("SELECT tag_id FROM photo_tags WHERE photo_id = :id", "id", photo.Id));

		while (reader.Read ()) {
			uint tag_id = Convert.ToUInt32 (reader ["tag_id"]);
			Tag tag = Core.Database.Tags.Get (tag_id) as Tag;
			photo.AddTagUnsafely (tag);
		}
		reader.Close();
	}		
	
	private void GetAllVersions  () {
		SqliteDataReader reader = Database.Query("SELECT photo_id, version_id, name, uri, md5_sum, protected FROM photo_versions");
		
		while (reader.Read ()) {
			uint id = Convert.ToUInt32 (reader ["photo_id"]);
			Photo photo = LookupInCache (id) as Photo;
				
			if (photo == null) {
				//Console.WriteLine ("Photo {0} not found", id);
				continue;
			}
				
			if (photo.Loaded) {
				//Console.WriteLine ("Photo {0} already Loaded", photo);
				continue;
			}

			if (reader ["version_id"] != null) {
				uint version_id = Convert.ToUInt32 (reader ["version_id"]);
				string name = reader["name"].ToString ();
#if MONO_2_0
				System.Uri uri = new System.Uri (reader["uri"].ToString ());
#else
				System.Uri uri = new System.Uri (reader["uri"].ToString (), true);
#endif
				string md5_sum = reader["md5_sum"].ToString ();
				bool is_protected = Convert.ToBoolean (reader["protected"]);	
				photo.AddVersionUnsafely (version_id, uri, md5_sum, name, is_protected);
			}

			/*
			string directory_path = null;
			if (reader [3] != null)
				directory_path = reader [3].ToString ();
			System.Console.WriteLine ("directory_path = {0}", directory_path);
			*/
		}
		reader.Close();
	}

	private void GetAllTags () {
		SqliteDataReader reader = Database.Query("SELECT photo_id, tag_id FROM photo_tags");

		while (reader.Read ()) {
			uint id = Convert.ToUInt32 (reader ["photo_id"]);
			Photo photo = LookupInCache (id) as Photo;
				
			if (photo == null) {
				//Console.WriteLine ("Photo {0} not found", id);
				continue;
			}
				
			if (photo.Loaded) {
				//Console.WriteLine ("Photo {0} already Loaded", photo.Id);
				continue;
			}

		        if (reader [1] != null) {
				uint tag_id = Convert.ToUInt32 (reader ["tag_id"]);
				Tag tag = Core.Database.Tags.Get (tag_id) as Tag;
				photo.AddTagUnsafely (tag);
			}
		}
		reader.Close();
	}

	public override DbItem Get (uint id)
	{
		Photo photo = LookupInCache (id) as Photo;
		if (photo != null)
			return photo;

		SqliteDataReader reader = Database.Query(
			new DbCommand("SELECT time, uri, description, roll_id, default_version_id, rating, md5_sum " + 
				      "FROM photos " + 
				      "WHERE id = :id", "id", id
				     )
		);

		if (reader.Read ()) {
			photo = new Photo (id,
				Convert.ToInt64 (reader ["time"]),
#if MONO_2_0
				new System.Uri (reader ["uri"].ToString ()),
#else
				new System.Uri (reader ["uri"].ToString (), true),
#endif
				reader["md5_sum"] != null ? reader["md5_sum"].ToString () : null
			);

			photo.Description = reader["description"].ToString ();
			photo.RollId = Convert.ToUInt32 (reader["roll_id"]);
			photo.DefaultVersionId = Convert.ToUInt32 (reader["default_version_id"]);
			photo.Rating = Convert.ToUInt32 (reader ["rating"]);
			AddToCache (photo);
		}
		reader.Close();

		if (photo == null)
			return null;
		
		GetTags (photo);
		GetVersions (photo);

		return photo;
	}

	[Obsolete ("Use GetByUri instead")]
	public Photo GetByPath (string path)
	{
		return GetByUri (UriUtils.PathToFileUri (path));
	}

	public Photo GetByUri (System.Uri uri)
	{
		Photo photo = null;

		uint timer = Log.DebugTimerStart ();

		SqliteDataReader reader = Database.Query (new DbCommand ("SELECT id, time, description, roll_id, default_version_id, rating, md5_sum " + 
									 " FROM photos " +
									 " LEFT JOIN photo_versions AS pv ON photos.id = pv.photo_id" +
							                 " WHERE photos.uri = :uri OR pv.uri = :uri", "uri", uri.ToString ()));

		if (reader.Read ()) {
			photo = new Photo (Convert.ToUInt32 (reader ["id"]),
					   Convert.ToInt64 (reader ["time"]),
					   uri,
					   reader["md5_sum"] != null ? reader["md5_sum"].ToString () : null);

			photo.Description = reader["description"].ToString ();
			photo.RollId = Convert.ToUInt32 (reader["roll_id"]);
			photo.DefaultVersionId = Convert.ToUInt32 (reader["default_version_id"]);
			photo.Rating = Convert.ToUInt32 (reader ["rating"]);
		}
	        reader.Close();
		Log.DebugTimerPrint (timer, "GetByUri query took {0}");

		if (photo == null)
			return null;

		Photo cached = LookupInCache (photo.Id) as Photo;

		if (cached != null)
			return cached;<commit_msg>minor fix related to the previous change<commit_after>/*
 * PhotoStore.cs
 *
 * Author(s):
 *	<NAME> <<EMAIL>>
 *	<NAME> <<EMAIL>>
 *	<NAME> <<EMAIL>>
 * 
 * This is free software. See COPYING for details.
 */

using Gnome;
using Gnome.Vfs;
using Gdk;
using Gtk;

using Mono.Data.SqliteClient;
using Mono.Unix;

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System;

using FSpot;
using FSpot.Query;
using FSpot.Utils;

using Banshee.Database;


public class PhotoStore : DbStore {
	public int TotalPhotos {
		get {
			SqliteDataReader reader = Database.Query("SELECT COUNT(*) AS photo_count FROM photos");
			reader.Read ();
			int total = Convert.ToInt32 (reader ["photo_count"]);
			reader.Close ();
			return total;
		}
	}

	public static ThumbnailFactory ThumbnailFactory = new ThumbnailFactory (ThumbnailSize.Large);

	// FIXME this is a hack.  Since we don't have Gnome.ThumbnailFactory.SaveThumbnail() in
	// GTK#, and generate them by ourselves directly with Gdk.Pixbuf, we have to make sure here
	// that the "large" thumbnail directory exists.
	private static void EnsureThumbnailDirectory ()
	{
		string large_thumbnail_file_name_template = Thumbnail.PathForUri ("file:///boo", ThumbnailSize.Large);
		string large_thumbnail_directory_path = System.IO.Path.GetDirectoryName (large_thumbnail_file_name_template);

		if (! System.IO.File.Exists (large_thumbnail_directory_path))
			System.IO.Directory.CreateDirectory (large_thumbnail_directory_path);
	}

	//
	// Generates the thumbnail, returns the Pixbuf, and also stores it as a side effect
	//

	public static Pixbuf GenerateThumbnail (System.Uri uri)
	{
		using (FSpot.ImageFile img = FSpot.ImageFile.Create (uri)) {
			return GenerateThumbnail (uri, img);
		}
	}

	public static Pixbuf GenerateThumbnail (System.Uri uri, ImageFile img)
	{
		Pixbuf thumbnail = null;

		if (img is FSpot.IThumbnailContainer) {
			try {
				thumbnail = ((FSpot.IThumbnailContainer)img).GetEmbeddedThumbnail ();
			} catch (Exception e) {
				Log.DebugFormat ("Exception while loading embedded thumbail {0}", e.ToString ());
			}
		}

		// Save embedded thumbnails in a silightly invalid way so that we know to regnerate them later.
		if (thumbnail != null) {
			PixbufUtils.SaveAtomic (thumbnail, FSpot.ThumbnailGenerator.ThumbnailPath (uri), 
						"png", new string [] { null} , new string [] { null});
			//FIXME with gio, set it to uri time minus a few sec
			System.IO.File.SetLastWriteTime (FSpot.ThumbnailGenerator.ThumbnailPath (uri), new DateTime (1980, 1, 1));
		} else 
			thumbnail = FSpot.ThumbnailGenerator.Create (uri);
		
		return thumbnail;
	}

	public static void DeleteThumbnail (System.Uri uri)
	{
		string path = Thumbnail.PathForUri (uri.ToString (), ThumbnailSize.Large);
		if (System.IO.File.Exists (path))
			System.IO.File.Delete (path);
	}

	public static void MoveThumbnail (string old_path, string new_path)
	{
		System.IO.File.Move (ThumbnailGenerator.ThumbnailPath (UriUtils.PathToFileUri (old_path)),
			   ThumbnailGenerator.ThumbnailPath(UriUtils.PathToFileUri (new_path)));
	}

	// Constructor

	public PhotoStore (QueuedSqliteDatabase database, bool is_new)
		: base (database, false)
	{
		EnsureThumbnailDirectory ();

		if (! is_new)
			return;
		
		Database.ExecuteNonQuery ( 
			"CREATE TABLE photos (\n" +
			"	id			INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \n" +
			"	time			INTEGER NOT NULL, \n" +
			"	uri			STRING NOT NULL, \n" +
			"	description		TEXT NOT NULL, \n" +
			"	roll_id			INTEGER NOT NULL, \n" +
			"	default_version_id	INTEGER NOT NULL, \n" +
			"	rating			INTEGER NULL, \n" +
			"	md5_sum			TEXT NULL\n" +
			")");

		Database.ExecuteNonQuery (
			"CREATE TABLE photo_tags (\n" +
			"	photo_id	INTEGER, \n" +
			"       tag_id		INTEGER, \n" +
			"       UNIQUE (photo_id, tag_id)\n" +
			")");

		Database.ExecuteNonQuery (
			"CREATE TABLE photo_versions (\n"+
			"	photo_id	INTEGER, \n" +
			"	version_id	INTEGER, \n" +
			"	name		STRING, \n" +
			"	uri		STRING NOT NULL, \n" +
			"	md5_sum		STRING NOT NULL, \n" +
			"	protected	BOOLEAN, \n" +
			"	UNIQUE (photo_id, version_id)\n" +
			")");

		Database.ExecuteNonQuery ("CREATE INDEX idx_photo_versions_id ON photo_versions(photo_id)");
		Database.ExecuteNonQuery ("CREATE INDEX idx_photos_roll_id ON photos(roll_id)");
	}

	public Photo CheckForDuplicate (System.Uri uri) {
		// Here we can go wild in comparing photos,
		// for now we check on uri and md5
		Photo found = GetByUri (uri);
		
		if (found != null)
		 	return found;

		string md5 = Photo.GenerateMD5 (uri);			
		Gnome.Vfs.FileInfo info = new Gnome.Vfs.FileInfo (uri.ToString (), FileInfoOptions.GetMimeType);

		Photo[] md5_matches = GetByMD5 (md5);

		foreach (Photo match in md5_matches)
		{
		 	Gnome.Vfs.FileInfo match_info = new Gnome.Vfs.FileInfo (match.DefaultVersionUri.ToString (), FileInfoOptions.GetMimeType);

			// same mimetype?
			if (info.MimeType != match_info.MimeType)
			 	continue;

			// other comparisons?

			// TODO? load pixbuf and compare sizes?	

			return match;
		}

		return null;
	}

	[Obsolete ("Use Create (Uri, uint, out Pixbuf) instead")]
	public Photo Create (string path, uint roll_id, out Pixbuf thumbnail)
	{
		return Create (path, path, roll_id, out thumbnail);
	}

	[Obsolete ("Use Create (Uri, Uri, uint, out Pixbuf) instead")]
	public Photo Create (string new_path, string orig_path, uint roll_id, out Pixbuf thumbnail)
	{
		return Create (UriUtils.PathToFileUri (new_path), UriUtils.PathToFileUri (orig_path), roll_id, out thumbnail);
	}

	public Photo Create (System.Uri uri, uint roll_id, out Pixbuf thumbnail)
	{
		return Create (uri, uri, roll_id, out thumbnail);
	}

	public Photo Create (System.Uri new_uri, System.Uri orig_uri, uint roll_id, out Pixbuf thumbnail)
	{
		Photo photo;
		using (FSpot.ImageFile img = FSpot.ImageFile.Create (orig_uri)) {
			long unix_time = DbUtils.UnixTimeFromDateTime (img.Date);
			string description = img.Description != null  ? img.Description.Split ('\0') [0] : String.Empty;
			string md5_sum = Photo.GenerateMD5 (new_uri);

	 		uint id = (uint) Database.Execute (
				new DbCommand (
					"INSERT INTO photos (time, uri, description, roll_id, default_version_id, rating, md5_sum) "	+
	 				"VALUES (:time, :uri, :description, :roll_id, :default_version_id, :rating, :md5_sum)",
	 				"time", unix_time,
					"uri", new_uri.OriginalString,
	 				"description", description,
					"roll_id", roll_id,
	 				"default_version_id", Photo.OriginalVersionId,
					"rating", "0",
					"md5_sum", md5_sum
				)
			);
	
			photo = new Photo (id, unix_time, new_uri, md5_sum);
			AddToCache (photo);
			photo.Loaded = true;
	
			thumbnail = GenerateThumbnail (new_uri, img);		
			EmitAdded (photo);
		}
		return photo;
	}


	private void GetVersions (Photo photo)
	{
		SqliteDataReader reader = Database.Query(
			new DbCommand("SELECT version_id, name, uri, md5_sum, protected " + 
				      "FROM photo_versions " + 
				      "WHERE photo_id = :id", 
				      "id", photo.Id
			)
		);

		while (reader.Read ()) {
			uint version_id = Convert.ToUInt32 (reader ["version_id"]);
			string name = reader["name"].ToString ();
#if MONO_2_0
			System.Uri uri = new System.Uri (reader["uri"].ToString ());
#else
			System.Uri uri = new System.Uri (reader["uri"].ToString (), true);
#endif
			string md5_sum = reader["md5_sum"].ToString ();
			bool is_protected = Convert.ToBoolean (reader["protected"]);
			photo.AddVersionUnsafely (version_id, uri, md5_sum, name, is_protected);
		}
		reader.Close();
	}

	private void GetTags (Photo photo)
	{
		SqliteDataReader reader = Database.Query(new DbCommand("SELECT tag_id FROM photo_tags WHERE photo_id = :id", "id", photo.Id));

		while (reader.Read ()) {
			uint tag_id = Convert.ToUInt32 (reader ["tag_id"]);
			Tag tag = Core.Database.Tags.Get (tag_id) as Tag;
			photo.AddTagUnsafely (tag);
		}
		reader.Close();
	}		
	
	private void GetAllVersions  () {
		SqliteDataReader reader = Database.Query("SELECT photo_id, version_id, name, uri, md5_sum, protected FROM photo_versions");
		
		while (reader.Read ()) {
			uint id = Convert.ToUInt32 (reader ["photo_id"]);
			Photo photo = LookupInCache (id) as Photo;
				
			if (photo == null) {
				//Console.WriteLine ("Photo {0} not found", id);
				continue;
			}
				
			if (photo.Loaded) {
				//Console.WriteLine ("Photo {0} already Loaded", photo);
				continue;
			}

			if (reader ["version_id"] != null) {
				uint version_id = Convert.ToUInt32 (reader ["version_id"]);
				string name = reader["name"].ToString ();
#if MONO_2_0
				System.Uri uri = new System.Uri (reader["uri"].ToString ());
#else
				System.Uri uri = new System.Uri (reader["uri"].ToString (), true);
#endif
				string md5_sum = reader["md5_sum"].ToString ();
				bool is_protected = Convert.ToBoolean (reader["protected"]);	
				photo.AddVersionUnsafely (version_id, uri, md5_sum, name, is_protected);
			}

			/*
			string directory_path = null;
			if (reader [3] != null)
				directory_path = reader [3].ToString ();
			System.Console.WriteLine ("directory_path = {0}", directory_path);
			*/
		}
		reader.Close();
	}

	private void GetAllTags () {
		SqliteDataReader reader = Database.Query("SELECT photo_id, tag_id FROM photo_tags");

		while (reader.Read ()) {
			uint id = Convert.ToUInt32 (reader ["photo_id"]);
			Photo photo = LookupInCache (id) as Photo;
				
			if (photo == null) {
				//Console.WriteLine ("Photo {0} not found", id);
				continue;
			}
				
			if (photo.Loaded) {
				//Console.WriteLine ("Photo {0} already Loaded", photo.Id);
				continue;
			}

		        if (reader [1] != null) {
				uint tag_id = Convert.ToUInt32 (reader ["tag_id"]);
				Tag tag = Core.Database.Tags.Get (tag_id) as Tag;
				photo.AddTagUnsafely (tag);
			}
		}
		reader.Close();
	}

	public override DbItem Get (uint id)
	{
		Photo photo = LookupInCache (id) as Photo;
		if (photo != null)
			return photo;

		SqliteDataReader reader = Database.Query(
			new DbCommand("SELECT time, uri, description, roll_id, default_version_id, rating, md5_sum " + 
				      "FROM photos " + 
				      "WHERE id = :id", "id", id
				     )
		);

		if (reader.Read ()) {
			photo = new Photo (id,
				Convert.ToInt64 (reader ["time"]),
#if MONO_2_0
				new System.Uri (reader ["uri"].ToString ()),
#else
				new System.Uri (reader ["uri"].ToString (), true),
#endif
				reader["md5_sum"] != null ? reader["md5_sum"].ToString () : null
			);

			photo.Description = reader["description"].ToString ();
			photo.RollId = Convert.ToUInt32 (reader["roll_id"]);
			photo.DefaultVersionId = Convert.ToUInt32 (reader["default_version_id"]);
			photo.Rating = Convert.ToUInt32 (reader ["rating"]);
			AddToCache (photo);
		}
		reader.Close();

		if (photo == null)
			return null;
		
		GetTags (photo);
		GetVersions (photo);

		return photo;
	}

	[Obsolete ("Use GetByUri instead")]
	public Photo GetByPath (string path)
	{
		return GetByUri (UriUtils.PathToFileUri (path));
	}

	public Photo GetByUri (System.Uri uri)
	{
		Photo photo = null;

		uint timer = Log.DebugTimerStart ();

		SqliteDataReader reader = Database.Query (new DbCommand ("SELECT id, time, description, roll_id, default_version_id, rating, photos.md5_sum AS md5_sum " +
									 " FROM photos " +
									 " LEFT JOIN photo_versions AS pv ON photos.id = pv.photo_id" +
							                 " WHERE photos.uri = :uri OR pv.uri = :uri", "uri", uri.ToString ()));

		if (reader.Read ()) {
			photo = new Photo (Convert.ToUInt32 (reader ["id"]),
					   Convert.ToInt64 (reader ["time"]),
					   uri,
					   reader["md5_sum"] != null ? reader["md5_sum"].ToString () : null);

			photo.Description = reader["description"].ToString ();
			photo.RollId = Convert.ToUInt32 (reader["roll_id"]);
			photo.DefaultVersionId = Convert.ToUInt32 (reader["default_version_id"]);
			photo.Rating = Convert.ToUInt32 (reader ["rating"]);
		}
	        reader.Close();
		Log.DebugTimerPrint (timer, "GetByUri query took {0}");

		if (photo == null)
			return null;

		Photo cached = LookupInCache (photo.Id) as Photo;

		if (cached != null)
			return cached;
{'fraction_non_alphanumeric': 0.13035758526715666, 'fraction_numerical': 0.007226030225452143, 'mean_word_length': 3.4941547596956766, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7512125', 'n_tokens_mistral': 9442, 'n_tokens_neox': 8089, 'n_words': 2155}