{"id":4880,"date":"2026-10-26T10:25:38","date_gmt":"2026-10-26T17:25:38","guid":{"rendered":"https:\/\/www.flamingspork.com\/blog\/?p=4880"},"modified":"2026-10-26T10:25:38","modified_gmt":"2026-10-26T17:25:38","slug":"swift-code-for-talking-to-the-shotwell-database","status":"publish","type":"post","link":"https:\/\/www.flamingspork.com\/blog\/2026\/10\/26\/swift-code-for-talking-to-the-shotwell-database\/","title":{"rendered":"Swift code for talking to the Shotwell database"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A flashback to a few years in the past, this has sat as a draft for too long. See previously: <a href=\"https:\/\/www.flamingspork.com\/blog\/2023\/06\/25\/getting-your-photos-out-of-shotwell\/\">Getting your photos out of Shotwell<\/a> and <a href=\"https:\/\/www.flamingspork.com\/blog\/2022\/02\/02\/every-time-i-program-a-mac\/\">Every time I program a Mac\u2026<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So&#8230;. what does the code to read the Shotwell database look like?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, we&#8217;re going to have to import some of the modules we need:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import Foundation\nimport SQLite3\nimport SwiftUI\nimport Photos<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I forget what SwiftUI here is for&#8230; I think it&#8217;s for my bit of integration with the UI I have.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From here I can start to model the bits of the Shotwell database that I&#8217;ll possibly need, but using more idiomatic Swift than raw SQL queries in random places. This is what I ended up for having information about a Photo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ShotwellPhotoInfo : Codable, Identifiable, Hashable {\n    static func == (lhs: ShotwellPhotoInfo, rhs: ShotwellPhotoInfo) -&gt; Bool {\n        return lhs.id == rhs.id\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n    }\n    \n    var id : Int32 = 0\n    var filename : String = \"\"\n    var translated_filename : String = \"\"\n    var orientation : Int32 = 0\n    var original_orientation : Int32 = 0\n    var import_id : Int32 = 0\n    var event_id : Int32 = 0\n    var event : ShotwellEventInfo?\n    var transformations : String = \"\"\n    var md5 : String = \"\"\n    var flags: Int32 = 0\n    var rating: Int32 = 0\n    var title: String = \"\"\n    var raw_backlinks : String = \"\"\n    var backlinks : ShotwellBacklinks?\n    var comment : String = \"\"\n    var has_gps : Bool = false\n    var gps_lat : Double = 0.0\n    var gps_lon : Double = 0.0\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The comparison function is just comparing the id, which since that&#8217;s a primary key in the database, are unique. The hash function also just acts on id because of this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The translated_filename is not in the Shotwell database, but rather for me to do various transformations on the filename in the shotwell database so that I can actually find the file. The natural translation is going to be for wherever I have mounted my photos, but there were some other weirdness likely due to some symlinks and such I&#8217;ve had. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Shotwell Database Architecture documentation says this:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<ul class=\"wp-block-list\">\n<li>backlinks are persistent links to container objects, i.e. Events and Tags. These are used when a Photo has been removed from one (for example, it&#8217;s been trashed or marked offline). If the Photo is returned to the main heap of photos (i.e. untrashed), these backlinks are used to restore its connections to the containers.<\/li>\n<\/ul>\n<cite>https:\/\/wiki.gnome.org\/Apps\/Shotwell\/Architecture\/Database<\/cite><\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">So it appears that Backlinks may be rather useful to find out everything I need to know about a Photo that I am looking to import. Thus, we will need to model Events and Tags as well, which are familiar concepts from the Shotwell UI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since there are Tags in Shotwell, I need to model them as well:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ShotwellTag : Codable, Identifiable, Hashable {\n    static func == (lhs: ShotwellTag, rhs: ShotwellTag) -&gt; Bool {\n        return lhs.id == rhs.id\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n    }\n    let id : Int32\n    let name : String\n    let photo_id_list : &#91;Int32]\n    let time_created : Int32\n    let photos : &#91;ShotwellPhotoInfo]\n    init(id: Int32, name: String, photo_id_list: &#91;Int32], time_created: Int32, photos: &#91;ShotwellPhotoInfo]) {\n        self.id = id\n        self.name = name\n        self.photo_id_list = photo_id_list\n        self.time_created = time_created\n        self.photos = photos\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Again, we have an id as a primary key, and the photos in a tag are referenced as a list of IDs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For an Event, this is what I ended up with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct ShotwellEventInfo : Codable, Identifiable {\n    let id : Int32\n    let name : String\n    let primary_photo_id : Int32\n    let time_created : Int32\n    let primary_source_id : Int32\n    let comment : String\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and for backlinks:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ShotwellBacklinks : Codable {\n    var tag : &#91;ShotwellTag?] = &#91;]\n    var event : &#91;String] = &#91;]\n    var other : &#91;String] = &#91;]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">But as for just getting some basic information out of the database? That we can start off with by having a class for opening it, and run some simple queries, and even display them in the UI just to prove we can.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ShotwellDatabase {\n    var db: OpaquePointer?\n    var import_query: OpaquePointer?\n    var dbPath : String?\n    \n    struct db_version {\n        var schema_version: Int32 = 0\n        var app_version: String = \"\"\n        init() {\n            schema_version = 0\n            app_version = \"\"\n        }\n    }\n    \n    func open_db(shotwellDB: String) -&gt; Bool {\n        dbPath = shotwellDB\n        if sqlite3_open(shotwellDB, &amp;db) == SQLITE_OK {\n            print(\"Opened Shotwell Database!\")\n        }\n        return db != nil\n    }\n    \n    func get_db_version() -&gt; db_version {\n        let query = \"SELECT schema_version, app_version FROM VersionTable\"\n        var queryStatement: OpaquePointer?\n        var r = db_version()\n        \n        if sqlite3_prepare_v2(db, query, -1, &amp;queryStatement, nil) == SQLITE_OK {\n            if sqlite3_step(queryStatement) == SQLITE_ROW {\n                r.schema_version = sqlite3_column_int(queryStatement, 0)\n                \n                guard let app_version = sqlite3_column_text(queryStatement, 1) else {\n                    print(\"Couldn't get app version\")\n                    r.app_version = \"\"\n                    return r\n                }\n                \n                r.app_version = String(cString: app_version)\n            }\n        }\n        return r\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The database version is more about just verifying I could link things together than anything actually useful. Continuing the ShotwellDatabase class, getting the total number of photos is pretty easy (added to the ShotwellDatabase class above):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    \n    func get_photos_count() -&gt; Int64 {\n        let query = \"SELECT COUNT(id) FROM PhotoTable\"\n        var queryStatement: OpaquePointer?\n\n        if sqlite3_prepare_v2(db, query, -1, &amp;queryStatement, nil) == SQLITE_OK {\n            if sqlite3_step(queryStatement) == SQLITE_ROW {\n                return sqlite3_column_int64(queryStatement, 0)\n            }\n        }\n        return 0\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Again, that&#8217;s mostly just useful for some nice &#8220;oh look, I linked up the UI to the database&#8221; rather than anything of use (although progress bars may want to use it).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So, now that we can talk to the database, we can go and set things up to iterate over all the photos and load them up into memory. I&#8217;m not doing anything too fancy here as this is all very quick and dirty, and was literally the first time writing any Swift code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The below code starts by opening the Database, then get_next_photo will iterate over everything before done_all_photos can close things up. The commented out block of code below does something pretty neat, and that&#8217;s print out a JSON representation of our photo. I found this <strong>incredibly<\/strong> easy to set up in Swift.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    func start_get_all_photos() -&gt; OpaquePointer? {\n        var db: OpaquePointer?\n        if sqlite3_open(dbPath, &amp;db) == SQLITE_OK {\n            print(\"Opened Shotwell Database!\")\n        }\n        start_import(db: db)\n        return db\n    }\n    func get_next_photo(db: OpaquePointer?) -&gt; ShotwellPhotoInfo? {\n        var photo: ShotwellPhotoInfo?\n            do {\n                photo = db_get_next_photo(db: db)\n                if photo != nil {\n<strong>\/*                    let encoder = JSONEncoder()\n                    encoder.outputFormatting = .prettyPrinted\n                    let jsonData = try encoder.encode(photo)\n                    let jsonString = String(data: jsonData, encoding: .utf8)!\n                    photo_info = jsonString\n                    print(jsonString)\n                    do_some_photo_things()\n                    progress += 1 *\/<\/strong>\n                    all_photos&#91;photo!.id] = photo\n                }\n            } catch { print(error) }\n        return photo\n    }\n    func done_all_photos(db: OpaquePointer?) {\n        sqlite3_close(db)\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For parsing that row, I have the following class:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ShotwellPhotoInfo : Codable, Identifiable, Hashable {\n    static func == (lhs: ShotwellPhotoInfo, rhs: ShotwellPhotoInfo) -&gt; Bool {\n        return lhs.id == rhs.id\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(id)\n    }\n    \n    var id : Int32 = 0\n    var filename : String = \"\"\n    var translated_filename : String = \"\"\n    var orientation : Int32 = 0\n    var original_orientation : Int32 = 0\n    var import_id : Int32 = 0\n    var event_id : Int32 = 0\n    var event : ShotwellEventInfo?\n    var transformations : String = \"\"\n    var md5 : String = \"\"\n    var flags: Int32 = 0\n    var rating: Int32 = 0\n    var title: String = \"\"\n    var raw_backlinks : String = \"\"\n    var backlinks : ShotwellBacklinks?\n    var comment : String = \"\"\n    var has_gps : Bool = false\n    var gps_lat : Double = 0.0\n    var gps_lon : Double = 0.0\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"> and function in the ShotwellDatabase class:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    func db_get_next_photo(db: OpaquePointer?) -&gt; ShotwellPhotoInfo? {\n        var r = ShotwellPhotoInfo()\n        if sqlite3_step(import_query) == SQLITE_ROW {\n            r.id = sqlite3_column_int(import_query, 0)\n            r.filename = String(cString: sqlite3_column_text(import_query, 1))\n            r.orientation = sqlite3_column_int(import_query, 2)\n            r.original_orientation = sqlite3_column_int(import_query, 3)\n            r.import_id = sqlite3_column_int(import_query, 4)\n            r.event_id = sqlite3_column_int(import_query, 5)\n\/\/ FIXME:            r.event = get_event_info(db: db, id: r.event_id)\n            let transformation = sqlite3_column_text(import_query, 6)\n            if let t = transformation {\n                r.transformations = String(cString: t)\n            }\n            let md5 = sqlite3_column_text(import_query, 7)\n            if let t = md5 {\n                r.md5 = String(cString: t)\n            }\n            r.flags = sqlite3_column_int(import_query, 8)\n            r.rating = sqlite3_column_int(import_query, 9)\n            let title = sqlite3_column_text(import_query, 10)\n            if let t = title {\n                r.title = String(cString: t)\n            }\n            let backlinks = sqlite3_column_text(import_query, 11)\n            if let t = backlinks {\n                r.raw_backlinks = String(cString: t)\n                r.backlinks = process_backlinks(all_backlinks: r.raw_backlinks)\n            }\n            let comment = sqlite3_column_text(import_query, 12)\n            if let t = comment {\n                r.comment = String(cString: t)\n            }\n            r.has_gps = sqlite3_column_int(import_query, 13) != 0\n            r.gps_lat = sqlite3_column_double(import_query, 14)\n            r.gps_lon = sqlite3_column_double(import_query, 15)\n        } else {\n            return nil\n        }\n        return r\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So with that combined, I ended up with my global <code>var all_photos: [Int32: ShotwellPhotoInfo] = [:]<\/code> containing all the photos in the shotwell database able to be looked up by ID. I&#8217;m sure there&#8217;s a more idiomatic way to do this in Swift&#8230; but the learning as I went, and need to only ever run this once certainly encouraged me to take some shortcuts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s two interesting bits in that code. There&#8217;s the process_backlinks function, which does the thing of processing this giant text string that Shotwell stores that points the photo back to everywhere it has been referenced, as well as the FIXME around event information. I ended up giving up on translating Events, as these were never something I used much.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Parsing the backlinks I did with this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    func process_backlinks(all_backlinks : String) -&gt; ShotwellBacklinks {\n        var r = ShotwellBacklinks()\n        let backlinks = all_backlinks.split(separator: \"\\n\").map( {String($0)})\n        \n        for backlink in backlinks {\n            let kv = backlink.split(separator: \"=\")\n            switch kv.first {\n            case \"tag\":\n                let tag_ids = kv&#91;1].split(separator: \"|\").map( {Int32($0)})\n                for tag_id in tag_ids {\n                    if tag_id != nil {\n                        if tag_id! &gt; 0 {\n                            r.tag.append(all_tags&#91;tag_id!])\n                        }\n                    }\n                }\n                break\n            case \"event\":\n                let events = kv&#91;1].split(separator: \"|\").map( {String($0)})\n                r.event = events\n                break\n            default:\n                r.other.append(contentsOf: kv.map({String($0)}))\n            }\n        }\n        \n        return r\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and the very simple ShotwellBacklinks class:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class ShotwellBacklinks : Codable {\n    var tag : &#91;ShotwellTag?] = &#91;]\n    var event : &#91;String] = &#91;]\n    var other : &#91;String] = &#91;]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But to start to get a view as to what Tags exist in the shotwell database, we can just go through the TagTable in the Shotwell SQLite database (added to the ShotwellDatabase class above):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    func get_all_tags() {\n        var db: OpaquePointer?\n        if sqlite3_open(dbPath, &amp;db) == SQLITE_OK {\n            print(\"Opened Shotwell Database!\")\n        }\n        let query = \"SELECT id, name, photo_id_list, time_created FROM TagTable\"\n        var q : OpaquePointer?\n        \n        if sqlite3_prepare_v2(db, query, -1, &amp;q, nil) != SQLITE_OK {\n            print(\"Failed to prepare query\")\n            assert(false)\n        }\n        \n        while sqlite3_step(q) == SQLITE_ROW {\n            let r = tag_row_to_ShotwellTag(q: q)\n            \n            all_tags&#91;r.id] = r\n            for photo_id in r.photo_id_list {\n                photoToTags&#91;photo_id]?.append(r)\n            }\n        }\n        sqlite3_finalize(q)\n        sqlite3_close(db)\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I did the hacky thing and just added <code>var all_tags: [Int32: ShotwellTag] = <\/code>[:] as a global variable, that I then used in the UI element showing them (well&#8230; via a wrapper, but that&#8217;s how that goes).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We just now need the tag_row_to_ShotwellTag that&#8217;s called above:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    func tag_row_to_ShotwellTag(q: OpaquePointer?) -&gt; ShotwellTag {\n        let id = sqlite3_column_int(q, 0)\n        let c_name = sqlite3_column_text(q, 1)\n        let c_photo_id_list = sqlite3_column_text(q, 2)\n        let time_created = sqlite3_column_int(q, 3  )\n        var name : String = \"\"\n        var photo_id_list : &#91;Int32] = &#91;]\n        var photos : &#91;ShotwellPhotoInfo] = &#91;]\n        \n        if let c_name = c_name {\n            name = String(cString: c_name)\n        }\n        \n        if let c_photo_id_list = c_photo_id_list {\n            let photo_ids = String(cString: c_photo_id_list).split(separator: \",\")\n            photo_id_list.reserveCapacity(photo_ids.count)\n            for photo_id in photo_ids {\n                if photo_id.hasPrefix(\"thumb\") {\n                    var id = photo_id\n                    id.removeFirst(5)\n                    photo_id_list.append(Int32(id, radix: 16)!)\n                    let photo = all_photos&#91;Int32(id, radix: 16)!]\n                    if photo == nil {\n                        print(\"Couldn't find photo id \" + id)\n                    } else {\n                        photos.append(photo!)\n                    }\n                } else {\n                    print(\"UNKNOWN PREFIX FOR \" + photo_id)\n                }\n            }\n        }\n        return ShotwellTag(id: id, name: name, photo_id_list: photo_id_list, time_created: time_created, photos: photos)\n    }\n    <\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A flashback to a few years in the past, this has sat as a draft for too long. See previously: Getting your photos out of Shotwell and Every time I program a Mac\u2026 So&#8230;. what does the code to read &hellip; <a href=\"https:\/\/www.flamingspork.com\/blog\/2026\/10\/26\/swift-code-for-talking-to-the-shotwell-database\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[1],"tags":[],"class_list":["post-4880","post","type-post","status-publish","format-standard","hentry","category-general"],"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p5a6n8-1gI","jetpack-related-posts":[{"id":4850,"url":"https:\/\/www.flamingspork.com\/blog\/2023\/06\/25\/getting-your-photos-out-of-shotwell\/","url_meta":{"origin":4880,"position":0},"title":"Getting your photos out of Shotwell","author":"Stewart Smith","date":"2023-06-25","format":false,"excerpt":"Somewhat a while ago now, I wrote about how every time I return to write some software for the Mac, the preferred language has changed. The purpose of this adventure was to get my photos out of the aging Shotwell and onto my (then new) Mac and the Apple Photos\u2026","rel":"","context":"In &quot;General&quot;","block_context":{"text":"General","link":"https:\/\/www.flamingspork.com\/blog\/category\/general\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2023\/06\/image.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2023\/06\/image.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2023\/06\/image.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2023\/06\/image.png?resize=700%2C400&ssl=1 2x"},"classes":[]},{"id":2160,"url":"https:\/\/www.flamingspork.com\/blog\/2010\/10\/25\/ubuntu-10-10-biggest-mistake-shotwell\/","url_meta":{"origin":4880,"position":1},"title":"Ubuntu 10.10 biggest mistake: shotwell","author":"Stewart Smith","date":"2010-10-25","format":false,"excerpt":"This is meant to replace f-spot. It just isn't ready. I do not have what I would consider a large photo collection. It's about 77GB on disk, maybe 30,000 images. Importing from f-spot is horrendously slow for what is essentially a few INSERT..SELECT statements. It does not copy your photos\u2026","rel":"","context":"In &quot;GNOME&quot;","block_context":{"text":"GNOME","link":"https:\/\/www.flamingspork.com\/blog\/category\/gnome\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":4839,"url":"https:\/\/www.flamingspork.com\/blog\/2022\/02\/02\/every-time-i-program-a-mac\/","url_meta":{"origin":4880,"position":2},"title":"Every time I program a Mac&#8230;","author":"Stewart Smith","date":"2022-02-02","format":false,"excerpt":"... the preferred programming language changes. I never programmed a 1980s Macintosh actually in the 1980s. It was sometime in the early 1990s that I first experienced Microsoft Basic for the Macintosh. I'd previously (unknowingly at the time as it was branded Commodore) experienced Microsoft BASIC on the Commodore 16,\u2026","rel":"","context":"In &quot;code&quot;","block_context":{"text":"code","link":"https:\/\/www.flamingspork.com\/blog\/category\/code\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2022\/02\/Screen-Shot-2022-02-02-at-12.52.15.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2022\/02\/Screen-Shot-2022-02-02-at-12.52.15.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2022\/02\/Screen-Shot-2022-02-02-at-12.52.15.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/www.flamingspork.com\/blog\/wp-content\/uploads\/2022\/02\/Screen-Shot-2022-02-02-at-12.52.15.png?resize=700%2C400&ssl=1 2x"},"classes":[]},{"id":3655,"url":"https:\/\/www.flamingspork.com\/blog\/2014\/01\/14\/converting-mysql-trees-to-git\/","url_meta":{"origin":4880,"position":3},"title":"Converting MySQL trees to git","author":"Stewart Smith","date":"2014-01-14","format":false,"excerpt":"I have put up a set of scripts on github: https:\/\/github.com\/stewartsmith\/bzr-to-git-conversion-scripts. Why do I need these? Well... if only bzr fast-export|git fast-import worked flawlessly for large, complex and old trees. It doesn't. Basically, when you clone this repo you can run \".\/sync-BLAH.sh\" and it'll pull BZR trees for the project,\u2026","rel":"","context":"In &quot;code&quot;","block_context":{"text":"code","link":"https:\/\/www.flamingspork.com\/blog\/category\/code\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":859,"url":"https:\/\/www.flamingspork.com\/blog\/2007\/07\/07\/evolution-import-vcf-possibly-the-slowest-thing-in-the-world\/","url_meta":{"origin":4880,"position":4},"title":"Evolution import VCF possibly the slowest thing in the world&#8230;","author":"Stewart Smith","date":"2007-07-07","format":false,"excerpt":"Waiting and waiting and waiting and waiting. It's only a few hundred cards in there.... urrggh.","rel":"","context":"In &quot;GNOME&quot;","block_context":{"text":"GNOME","link":"https:\/\/www.flamingspork.com\/blog\/category\/gnome\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":69,"url":"https:\/\/www.flamingspork.com\/blog\/2003\/03\/18\/free-open-diary-import\/","url_meta":{"origin":4880,"position":5},"title":"Free Open Diary Import","author":"Stewart Smith","date":"2003-03-18","format":false,"excerpt":"Well, I've imported all my old OpenDiary entries. Unfortunately, there's no easy way to export all the notes from OD, so they haven't made their way across. Below is the perl script I used to convert ODs export format into the MovableType import format: #!\/usr\/bin\/perl { open ODFILE,\"< $ARGV[0]\"; @entries=();\u2026","rel":"","context":"In &quot;life, the universe and everything&quot;","block_context":{"text":"life, the universe and everything","link":"https:\/\/www.flamingspork.com\/blog\/category\/life-the-universe-and-everything\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"jetpack_likes_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/4880","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/comments?post=4880"}],"version-history":[{"count":3,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/4880\/revisions"}],"predecessor-version":[{"id":5039,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/posts\/4880\/revisions\/5039"}],"wp:attachment":[{"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/media?parent=4880"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/categories?post=4880"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.flamingspork.com\/blog\/wp-json\/wp\/v2\/tags?post=4880"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}