diff --git a/foo_dop/cfobject.cpp b/foo_dop/cfobject.cpp index dca3ca8..1267ece 100644 --- a/foo_dop/cfobject.cpp +++ b/foo_dop/cfobject.cpp @@ -64,6 +64,28 @@ namespace cfobject p_value = (child->m_integer); return b_ret; } + + const pfc::string_simple_t& object_t::dictionary::get_child_string_strict(const wchar_t * key) + { + return get_child_strict(key); + } + + bool object_t::dictionary::get_child_bool_strict(const wchar_t * key, std::optional default_value) + { + return get_child_strict_with_default(key, default_value); + } + + int64_t object_t::dictionary::get_child_int64_strict(const wchar_t * key, std::optional default_value) + { + return get_child_strict_with_default(key, default_value); + } + + const std::vector& object_t::dictionary::get_child_array_strict(const wchar_t * key) + { + return get_child_strict(key); + } + + void object_t::dictionary::ensure_sorted() { if (!m_sorted) @@ -198,6 +220,7 @@ namespace cfobject }; } } + void g_export_object_to_xml (const object_t::ptr_t & ptr, pfc::string8 & p_out) { p_out.reset(); diff --git a/foo_dop/cfobject.h b/foo_dop/cfobject.h index 360b2ea..e8afae0 100644 --- a/foo_dop/cfobject.h +++ b/foo_dop/cfobject.h @@ -20,6 +20,37 @@ namespace cfobject public: typedef pfc::refcounted_object_ptr_t ptr_t; + template + auto& get_value_strict() + { + if (m_type != TypeTag) { + throw exception_io_unsupported_format(); + } + + if constexpr (TypeTag == kTagInt) + return m_integer; + + if constexpr (TypeTag == kTagArray) + return m_array; + + if constexpr (TypeTag == kTagBoolean) + return m_boolean; + + if constexpr (TypeTag == kTagDictionary) + return m_dictionary; + + if constexpr (TypeTag == kTagUnicodeString) + return m_string; + + if constexpr (TypeTag == kTagData) + return m_data; + + if constexpr (TypeTag == kTagReal) + return m_float; + + throw pfc::exception_bug_check(); + } + class dictionary_entry_t { public: @@ -48,6 +79,48 @@ namespace cfobject bool get_child (const wchar_t * key, t_uint64 & p_value); bool get_child (const wchar_t * key, t_int64 & p_value); + template + auto* get_child(const wchar_t* key) + { + object_t::ptr_t child; + if (get_child(key, child)) + return &child->get_value_strict(); + + using ValuePtr = decltype(&child->get_value_strict()); + return static_cast(nullptr); + } + + /** Note: This will copy the value (unlike get_child above) */ + template + auto get_child_strict_with_default(const wchar_t* key, Default&& default_value = {}) + { + using Type = std::remove_reference_t().get_value_strict())>; + std::optional default_value_as_optional{ std::forward(default_value) }; + auto child = get_child(key); + + if (!child && !default_value_as_optional) + throw exception_io_unsupported_format(); + + if (!child) + return default_value_as_optional.value(); + + return *child; + } + + template + auto& get_child_strict(const wchar_t* key) + { + auto child = get_child(key); + if (!child) + throw exception_io_unsupported_format(); + return *child; + } + + const pfc::string_simple_t& get_child_string_strict(const wchar_t * key); + bool get_child_bool_strict(const wchar_t * key, std::optional default_value = {}); + int64_t get_child_int64_strict(const wchar_t * key, std::optional default_value = {}); + const std::vector& get_child_array_strict(const wchar_t * key); + dictionary() : m_sorted(false) {}; private: void ensure_sorted(); diff --git a/foo_dop/foo_dop.vcxproj b/foo_dop/foo_dop.vcxproj index a88bc07..1ca77e3 100644 --- a/foo_dop/foo_dop.vcxproj +++ b/foo_dop/foo_dop.vcxproj @@ -225,6 +225,7 @@ + diff --git a/foo_dop/foo_dop.vcxproj.filters b/foo_dop/foo_dop.vcxproj.filters index 72adc98..ca3f886 100644 --- a/foo_dop/foo_dop.vcxproj.filters +++ b/foo_dop/foo_dop.vcxproj.filters @@ -407,6 +407,9 @@ Component + + Backend Operations + diff --git a/foo_dop/itunesdb.h b/foo_dop/itunesdb.h index 003d42c..8004729 100644 --- a/foo_dop/itunesdb.h +++ b/foo_dop/itunesdb.h @@ -20,6 +20,7 @@ t_uint32 apple_time_from_filetime(t_filetimestamp filetime, bool b_local = true); t_filetimestamp filetime_time_from_appletime(t_uint32 appletime, bool b_convert_to_utc = true); +t_uint32 current_hfs_plus_timestamp(); bool g_print_meta(const file_info & info, const char * field, pfc::string_base & p_out); bool g_print_meta_noblanks(const file_info & info, const char * field, pfc::string_base & p_out); diff --git a/foo_dop/itunesdb_helpers.cpp b/foo_dop/itunesdb_helpers.cpp index a37c346..95ffdcf 100644 --- a/foo_dop/itunesdb_helpers.cpp +++ b/foo_dop/itunesdb_helpers.cpp @@ -2,6 +2,7 @@ #include "ipod_manager.h" #include "writer_sort_helpers.h" +#include "file_adder.h" bool is_blank(const char * str) { @@ -113,6 +114,13 @@ LocalFileTimeToFileTime2( return SystemTimeToFileTime(&stUTC, lpFileTime); } +t_uint32 current_hfs_plus_timestamp() +{ + FILETIME ft{}; + GetSystemTimeAsFileTime(&ft); + return apple_time_from_filetime(g_filetime_to_timestamp(&ft)); +} + t_uint32 apple_time_from_filetime(t_filetimestamp filetime_src, bool b_local) { if (filetime_src == filetimestamp_invalid) return 0; diff --git a/foo_dop/itunesdb_playlist.cpp b/foo_dop/itunesdb_playlist.cpp index b685045..d653b49 100644 --- a/foo_dop/itunesdb_playlist.cpp +++ b/foo_dop/itunesdb_playlist.cpp @@ -161,6 +161,7 @@ namespace itunesdb { p_pihm.read_lendian_auto_t(p_out->items[i].unk1, p_abort); //36 p_pihm.read_lendian_auto_t(p_out->items[i].unk2, p_abort); //40 p_pihm.read_lendian_auto_t(p_out->items[i].item_pid, p_abort); //44 + // some 64-bit other ID at +60? } catch (exception_io_data_truncation const &) {}; diff --git a/foo_dop/reader.cpp b/foo_dop/reader.cpp index d11eb1c..083d553 100644 --- a/foo_dop/reader.cpp +++ b/foo_dop/reader.cpp @@ -590,6 +590,7 @@ namespace ipod if (!m_writing || !p_ipod->mobile) read_playcounts(p_ipod, p_abort); + load_device_playlists(p_ipod, p_abort); p_status.checkpoint(); diff --git a/foo_dop/reader.h b/foo_dop/reader.h index 669b7a7..740259f 100644 --- a/foo_dop/reader.h +++ b/foo_dop/reader.h @@ -16,6 +16,17 @@ namespace ipod namespace tasks { + struct DevicePlaylist { + int64_t playlist_persistent_id{}; + int64_t version{}; + bool playlist_deleted{}; + int64_t saved_index{}; + pfc::string8 name; + /** Appears to be the timestamp of iTunesDB (and not iTunesCDB or an .itdb file) */ + int64_t db_timestamp_mac_os_date{}; + std::vector track_persistent_ids; + }; + class load_database_t { class portable_device_playbackdata_notifier_impl : public dop::portable_device_playbackdata_notifier_t @@ -101,7 +112,19 @@ namespace ipod void read_storepurchases(ipod_device_ptr_ref_t p_ipod, abort_callback & p_abort); void read_storepurchases(ipod_device_ptr_ref_t p_ipod, store_purchases_type_t, abort_callback & p_abort); void read_playcounts(ipod_device_ptr_ref_t p_ipod, abort_callback & p_abort); - static int g_compare_playlist_id (const pfc::rcptr_t & item1, const pfc::rcptr_t & item2) + + /** + * For, at least, the nano 7G. + */ + void load_device_playlists(ipod_device_ptr_ref_t p_ipod, abort_callback & p_abort); + + /** + * For, at least, the nano 7G. + */ + void load_device_playlist(const DevicePlaylist& playlist); + void clean_up_device_playlists(); + + static int g_compare_playlist_id (const pfc::rcptr_t & item1, const pfc::rcptr_t & item2) { return pfc::compare_t(item1->id,item2->id); } @@ -122,13 +145,19 @@ namespace ipod return pfc::compare_t(item1->pid,dbid); } - bool have_track (t_uint64 pid) + pfc::rcptr_t get_track_by_pid(t_uint64 pid) { for (t_size i = 0, count = m_tracks.get_count(); ipid == pid) return true; + if (m_tracks[i]->pid == pid) + return m_tracks[i]; } - return false; + return {}; + } + + bool have_track(t_uint64 pid) + { + return get_track_by_pid(pid).is_valid(); }; void glue_items (t_size start); @@ -399,6 +428,7 @@ namespace ipod private: bool m_failed; bool m_writing; + pfc::list_t< pfc::string8 > m_read_device_playlists; }; } } diff --git a/foo_dop/reader_playlists.cpp b/foo_dop/reader_playlists.cpp new file mode 100644 index 0000000..6fe566e --- /dev/null +++ b/foo_dop/reader_playlists.cpp @@ -0,0 +1,129 @@ +#include "stdafx.h" + +#include "plist.h" + +static const std::wregex file_name_regex(L"Playlist_[A-Z0-9]{1,16}.plist"); + +namespace ipod::tasks +{ + +DevicePlaylist read_plist_otg_playlist(const char* path, abort_callback & p_abort) +{ + file::ptr f; + filesystem::g_open_read(f, path, p_abort); + auto parser = PlistParserFromFile(path, p_abort); + + if (parser.m_root_object.is_empty()) + throw exception_io_unsupported_format(); + + auto& dict = parser.m_root_object->get_value_strict(); + DevicePlaylist otg_playlist; + + otg_playlist.playlist_persistent_id = dict.get_child_int64_strict(L"playlistPersistentID"); + otg_playlist.saved_index = dict.get_child_int64_strict(L"savedIndex", 0); + otg_playlist.name = pfc::stringcvt::string_utf8_from_wide(dict.get_child_string_strict(L"name")); + otg_playlist.db_timestamp_mac_os_date = dict.get_child_int64_strict(L"dbTimestampMacOsDate"); + otg_playlist.playlist_deleted = dict.get_child_bool_strict(L"playlistDeleted", false); + auto& pid_objects = dict.get_child_array_strict(L"trackPersistentIds"); + + otg_playlist.track_persistent_ids.reserve(pid_objects.size()); + for (auto&& pid_object : pid_objects) { + otg_playlist.track_persistent_ids.emplace_back(pid_object->get_value_strict()); + } + + return otg_playlist; +} + +void load_database_t::load_device_playlists(ipod_device_ptr_ref_t p_ipod, abort_callback & p_abort) +{ + service_ptr_t p_counts_file; + + pfc::string8 database_folder; + p_ipod->get_database_path(database_folder); + pfc::string8 path = database_folder << "/iTunes/"; + pfc::list_t file_paths; + directory_callback_retrieveList callback(file_paths, true, false); + pfc::string8 canonical_path; + filesystem::g_get_canonical_path(path, canonical_path); + filesystem::g_list_directory(canonical_path, callback, p_abort); + + std::vector read_playlists; + + for (size_t i{0}; i < file_paths.get_count(); ++i) { + auto& file_path = file_paths[i]; + pfc::string_filename_ext name(file_path); + pfc::stringcvt::string_wide_from_utf8 wname(name); + + if (std::regex_search(wname.get_ptr(), file_name_regex)) { + read_playlists.emplace_back(read_plist_otg_playlist(file_path, p_abort)); + m_read_device_playlists.add_item(file_path); + } + } + + std::sort( + read_playlists.begin(), + read_playlists.end(), + [](auto&& left, auto&& right) { return left.saved_index < right.saved_index; } + ); + + for (auto&& otg_playlist : read_playlists) { + load_device_playlist(otg_playlist); + } +} + +void load_database_t::load_device_playlist(const DevicePlaylist& otg_playlist) +{ + if (otg_playlist.playlist_deleted) { + size_t playlist_index; + if (find_playlist_by_id(otg_playlist.playlist_persistent_id, playlist_index)) { + m_playlists.remove_by_idx(playlist_index); + } + return; + } + + const auto now = current_hfs_plus_timestamp(); + uint32_t index{}; + const bool is_new_playlist = !find_playlist_by_id(otg_playlist.playlist_persistent_id, index); + auto playlist = is_new_playlist ? pfc::rcnew_t() : m_playlists[index]; + playlist->id = static_cast(otg_playlist.playlist_persistent_id); + playlist->name = otg_playlist.name; + if (is_new_playlist) + playlist->timestamp = now; + playlist->date_modified = now; + + playlist->items.remove_all(); + for (auto&& pid : otg_playlist.track_persistent_ids) { + auto track = get_track_by_pid(pid); + if (track.is_empty()) + continue; + + t_playlist_entry entry; + entry.item_pid = pid; + entry.timestamp = track->dateadded; + entry.track_id = track->id; + playlist->items.add_item(entry); + } + + // In theory we would need to (re)generate VoiceOver files for playlist names, however + // those are only for iPod shuffles. + if (is_new_playlist) { + m_playlists.add_item(playlist); + } +} + +void load_database_t::clean_up_device_playlists() +{ + abort_callback_dummy aborter; + for (size_t i{0}; i < m_read_device_playlists.get_count(); ++i) { + auto&& path = m_read_device_playlists[i]; + try { + filesystem::g_remove(path, aborter); + } + catch (const exception_io& ex) { + console::formatter formatter; + formatter << "iPod manager: Failed to delete file: " << path; + } + } +} + +} // namespace ipod::tasks diff --git a/foo_dop/stdafx.h b/foo_dop/stdafx.h index f5a2e71..0eca01c 100644 --- a/foo_dop/stdafx.h +++ b/foo_dop/stdafx.h @@ -15,6 +15,9 @@ //#define LOAD_LIBRARY_INDICES //#define PHOTO_BROWSER +#include +#include + #include #include #include diff --git a/foo_dop/writer.cpp b/foo_dop/writer.cpp index 3e56474..c71df00 100644 --- a/foo_dop/writer.cpp +++ b/foo_dop/writer.cpp @@ -52,6 +52,11 @@ void database_writer_t::write_artworkdb(ipod_device_ptr_ref_t p_ipod, const ipod } } +void database_writer_t::clean_up_after_write(ipod::tasks::load_database_t& p_library) +{ + p_library.clean_up_device_playlists(); +} + void database_writer_t::run(ipod_device_ptr_ref_t p_ipod, ipod::tasks::load_database_t & m_library, const t_field_mappings & p_mappings, threaded_process_v2_t & p_status,abort_callback & p_abort) { //profiler (writer_run); @@ -139,6 +144,7 @@ void database_writer_t::run(ipod_device_ptr_ref_t p_ipod, ipod::tasks::load_data p_status.update_progress_subpart_helper(14 + (p_ipod->m_device_properties.m_SQLiteDB?15:0),15 + (p_ipod->m_device_properties.m_SQLiteDB?15:0)); m_library.save_cache(p_status.get_wnd(), p_ipod, p_status, p_abort); + clean_up_after_write(m_library); p_status.update_progress_subpart_helper(15,15); diff --git a/foo_dop/writer.h b/foo_dop/writer.h index 95ae82a..9da5bf8 100644 --- a/foo_dop/writer.h +++ b/foo_dop/writer.h @@ -183,6 +183,7 @@ namespace ipod void write_artworkdb (ipod_device_ptr_ref_t p_ipod, const ipod::tasks::load_database_t & p_library, threaded_process_v2_t & p_status,abort_callback & p_abort); void write_sqlitedb (ipod_device_ptr_ref_t p_ipod, ipod::tasks::load_database_t & p_library, const t_field_mappings & p_mappings, threaded_process_v2_t & p_status,abort_callback & p_abort); pfc::array_staticsize_t calculate_cbk(ipod_device_ptr_ref_t p_ipod, const char* locations_itdb_path); + void clean_up_after_write(ipod::tasks::load_database_t & p_library); //construct from main thread only database_writer_t() {