diff --git a/.changeset/nano-7g-firmware-1-0-1.md b/.changeset/nano-7g-firmware-1-0-1.md new file mode 100644 index 00000000..a2b8b2cf --- /dev/null +++ b/.changeset/nano-7g-firmware-1-0-1.md @@ -0,0 +1,5 @@ +--- +'@podkit/libgpod-node': patch +--- + +The iPod nano 7th generation on firmware 1.0.1 can now be written. That firmware lists only SQLite post-process command set 9, not set 25, so writes failed with "no command set 25"; libgpod now falls back to set 9 and skips carrying over track choice columns that set 9 does not create. diff --git a/tools/nano7/README.md b/tools/nano7/README.md index 5ba94323..8a1b8377 100644 --- a/tools/nano7/README.md +++ b/tools/nano7/README.md @@ -50,10 +50,12 @@ Audiobooks, Rentals) in `Library.itdb` and in the `iTunesCDB` dataset 5, whose header fields are kept as read, and iTunes' schema level: its additional columns, `user_version` 26 (`Library.itdb`) and 2 (`Genius.itdb`) and the `version_info` row. The device's SQLite post-process -command set 25 completes that schema. A missing set, a missing or failing -command, or an insert or commit that fails while the databases are generated -fails the write and leaves the library unchanged. A nano 5th generation keeps -libgpod's previous layout. +command set 25 completes that schema. Firmware 1.0.1 lists only its +predecessor, set 9, which lacks the track choice columns and +`FakePragmaITunes26`; set 9 is used when set 25 is absent. A missing set, a +missing or failing command, or an insert or commit that fails while the +databases are generated fails the write and leaves the library unchanged. A +nano 5th generation keeps libgpod's previous layout. A write keeps what a nano 6th/7th generation recorded in `Dynamic.itdb`. Plays, skips, ratings, bookmarks, recent counts and their dates become the diff --git a/tools/nano7/test_libgpod.py b/tools/nano7/test_libgpod.py index 4302d291..c3d5c2d6 100644 --- a/tools/nano7/test_libgpod.py +++ b/tools/nano7/test_libgpod.py @@ -350,6 +350,25 @@ def test_nano_post_process_commands_are_required(self): self.assertIn("post-process commands", result.stderr) self.assertEqual(self.snapshot(), library) + def test_nano_firmware_1_0_1_command_set_9(self): + # Firmware 1.0.1 lists only set 9: set 25 without its later commands, + # and device update level 1107. It has no track choice columns. + sets, sql = self.commands["UserVersionCommandSets"], self.commands["SQLCommands"] + later = {"CreateIsHomeVideo", "FakePragmaITunes26", "UpdateDeviceUpdateLevelTo1109"} + names = [name for name in sets["25"]["Commands"] + if name not in later and "ItemStatsAudio" not in name and "ItemStatsSubtitle" not in name] + set_9 = dict(sets["25"], Commands=names + ["UpdateDeviceUpdateLevelTo1107"]) + self.setup_synthetic("MC526", 5, dict( + self.commands, UserVersionCommandSets={"9": set_9}, + SQLCommands=dict(sql, UpdateDeviceUpdateLevelTo1107= + "UPDATE version_info SET device_update_level = 1107;"))) + self.run_helper("create") + self.run_helper("resave") + self.check_library(1) + self.assertEqual(rows(self.db("Library"), "SELECT device_update_level FROM version_info"), + [(1107,)]) + self.assertNotIn("audio_language", columns(self.db("Dynamic"), "item_stats")) + def test_nano_runs_the_first_genre_count_command(self): # Set 25 lists AddGenreMapCompilationCount twice; only the repeat is skipped. sql = dict(self.commands["SQLCommands"]) diff --git a/tools/prebuild/libgpod-nano.patch b/tools/prebuild/libgpod-nano.patch index e51b9816..53ce2733 100644 --- a/tools/prebuild/libgpod-nano.patch +++ b/tools/prebuild/libgpod-nano.patch @@ -955,9 +955,9 @@ while (subnode) { unsigned long int intval = strtoul(key, NULL, 0); - if ((intval > 0) && (intval > maxver)) { -+ /* nano 6G/7G: set 25 fits libgpod's schema plus -+ Library_nano_columns and raises it to iTunes' level 26 */ -+ if (nano ? (intval == 25) : ((intval > 0) && (intval > maxver))) { ++ /* nano 6G/7G: set 25 fits libgpod's schema plus Library_nano_columns; ++ firmware 1.0.1 lists only its predecessor, set 9 */ ++ if (nano ? (intval == 25 || (intval == 9 && maxver != 25)) : ((intval > 0) && (intval > maxver))) { user_ver_cmds = subnode; maxver = intval; } @@ -1094,7 +1094,7 @@ if (!mk_Locations_cbk(itdb, outpath)) { g_set_error (error, ITDB_ERROR, ITDB_ERROR_SQLITE, -@@ -2250,6 +2476,187 @@ static void rmdir_recursive(gchar *path) +@@ -2250,6 +2476,198 @@ static void rmdir_recursive(gchar *path) g_rmdir(path); } @@ -1241,6 +1241,17 @@ + g_free(itlpdir); +} + ++/* Command set 9 (nano 7G firmware 1.0.1) creates no track choice columns */ ++static gboolean has_track_choices(const char *dbf) ++{ ++ sqlite3 *db = NULL; ++ gboolean present = sqlite3_open(dbf, &db) == SQLITE_OK ++ && sqlite3_exec(db, "SELECT audio_language FROM item_stats LIMIT 0;", NULL, NULL, NULL) == SQLITE_OK; ++ ++ sqlite3_close(db); ++ return present; ++} ++ +/* iPod nano 6G/7G: keep the audio and subtitle track choices the device + recorded for items still in the library; the post-process commands + reset them. */ @@ -1258,7 +1269,7 @@ + "the plays, ratings and bookmarks it holds; remove it to write without them", + device_file); + res = -1; -+ } else if (g_file_test(device_file, G_FILE_TEST_EXISTS)) { ++ } else if (g_file_test(device_file, G_FILE_TEST_EXISTS) && has_track_choices(dbf)) { + /* The columns are unqualified: one that the device's item_stats + lacks names the new row's own, which keeps its value. */ + char *sql = sqlite3_mprintf("ATTACH DATABASE %Q AS device;" @@ -1282,7 +1293,7 @@ int itdb_sqlite_generate_itdbs(FExport *fexp) { int res = 0; -@@ -2277,6 +2684,7 @@ int itdb_sqlite_generate_itdbs(FExport *fexp) +@@ -2277,6 +2695,7 @@ int itdb_sqlite_generate_itdbs(FExport *fexp) g_assert(fexp->itdb->playlists != NULL); tzoffset = fexp->itdb->tzoffset; @@ -1290,7 +1301,7 @@ tmpdir = g_build_path(g_get_tmp_dir(), tmpnam(NULL), NULL); if (g_mkdir(tmpdir, 0755) != 0) { -@@ -2289,7 +2697,8 @@ int itdb_sqlite_generate_itdbs(FExport *fexp) +@@ -2289,7 +2708,8 @@ int itdb_sqlite_generate_itdbs(FExport *fexp) /* generate itdb files in temporary directory */ if (build_itdb_files(fexp->itdb, fexp->albums, fexp->artists, fexp->composers, tmpdir,