#include "Emu/Cell/PPUModule.h" #include "Emu/IdManager.h" #include "Emu/VFS.h" #include "cellMusic.h" #include "cellSysutil.h" #include "stdafx.h" #include #include "cellSearch.h" #include "util/StrUtil.h" #include "util/media_utils.h" #include LOG_CHANNEL(cellSearch); template <> void fmt_class_string::format(std::string& out, u64 arg) { format_enum(out, arg, [](auto error) { switch (error) { STR_CASE(CELL_SEARCH_CANCELED); STR_CASE(CELL_SEARCH_ERROR_PARAM); STR_CASE(CELL_SEARCH_ERROR_BUSY); STR_CASE(CELL_SEARCH_ERROR_NO_MEMORY); STR_CASE(CELL_SEARCH_ERROR_UNKNOWN_MODE); STR_CASE(CELL_SEARCH_ERROR_ALREADY_INITIALIZED); STR_CASE(CELL_SEARCH_ERROR_NOT_INITIALIZED); STR_CASE(CELL_SEARCH_ERROR_FINALIZING); STR_CASE(CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH); STR_CASE(CELL_SEARCH_ERROR_CONTENT_OBSOLETE); STR_CASE(CELL_SEARCH_ERROR_CONTENT_NOT_FOUND); STR_CASE(CELL_SEARCH_ERROR_NOT_LIST); STR_CASE(CELL_SEARCH_ERROR_OUT_OF_RANGE); STR_CASE(CELL_SEARCH_ERROR_INVALID_SEARCHID); STR_CASE(CELL_SEARCH_ERROR_ALREADY_GOT_RESULT); STR_CASE(CELL_SEARCH_ERROR_NOT_SUPPORTED_CONTEXT); STR_CASE(CELL_SEARCH_ERROR_INVALID_CONTENTTYPE); STR_CASE(CELL_SEARCH_ERROR_DRM); STR_CASE(CELL_SEARCH_ERROR_TAG); STR_CASE(CELL_SEARCH_ERROR_GENERIC); } return unknown; }); } enum class search_state { not_initialized = 0, idle, in_progress, initializing, canceling, finalizing, }; struct search_info { vm::ptr func; vm::ptr userData; atomic_t state = search_state::not_initialized; shared_mutex links_mutex; struct link_data { std::string path; bool is_dir = false; }; std::unordered_map content_links; }; struct search_content_t { CellSearchContentType type = CELL_SEARCH_CONTENTTYPE_NONE; CellSearchRepeatMode repeat_mode = CELL_SEARCH_REPEATMODE_NONE; CellSearchContextOption context_option = CELL_SEARCH_CONTEXTOPTION_NONE; CellSearchTimeInfo time_info; CellSearchContentInfoPath infoPath; union { CellSearchMusicInfo music; CellSearchPhotoInfo photo; CellSearchVideoInfo video; CellSearchMusicListInfo music_list; CellSearchPhotoListInfo photo_list; CellSearchVideoListInfo video_list; CellSearchVideoSceneInfo scene; } data; ENABLE_BITWISE_SERIALIZATION; }; using content_id_type = std::pair>; struct content_id_map { std::unordered_map> map; shared_mutex mutex; SAVESTATE_INIT_POS(36); }; struct search_object_t { // TODO: Figured out the correct values to set here static const u32 id_base = 1; static const u32 id_step = 1; static const u32 id_count = 1024; // TODO SAVESTATE_INIT_POS(36.1); std::vector content_ids; }; static const std::string link_base = "/dev_hdd0/.tmp/"; // WipEout HD does not like it if we return a path // starting with "/.tmp", so let's use "/dev_hdd0" error_code check_search_state(search_state state, search_state action) { switch (action) { case search_state::initializing: switch (state) { case search_state::not_initialized: break; case search_state::initializing: return CELL_SEARCH_ERROR_BUSY; case search_state::finalizing: return CELL_SEARCH_ERROR_FINALIZING; default: return CELL_SEARCH_ERROR_ALREADY_INITIALIZED; } break; case search_state::finalizing: switch (state) { case search_state::idle: break; case search_state::not_initialized: return CELL_SEARCH_ERROR_NOT_INITIALIZED; case search_state::finalizing: return CELL_SEARCH_ERROR_FINALIZING; case search_state::in_progress: case search_state::initializing: case search_state::canceling: return CELL_SEARCH_ERROR_BUSY; default: return CELL_SEARCH_ERROR_GENERIC; } break; case search_state::canceling: switch (state) { case search_state::in_progress: break; case search_state::not_initialized: case search_state::initializing: return CELL_SEARCH_ERROR_NOT_INITIALIZED; case search_state::finalizing: return CELL_SEARCH_ERROR_FINALIZING; case search_state::canceling: return CELL_SEARCH_ERROR_BUSY; case search_state::idle: return CELL_SEARCH_ERROR_ALREADY_GOT_RESULT; default: return CELL_SEARCH_ERROR_GENERIC; } break; case search_state::in_progress: default: switch (state) { case search_state::idle: break; case search_state::not_initialized: case search_state::initializing: return CELL_SEARCH_ERROR_NOT_INITIALIZED; case search_state::finalizing: return CELL_SEARCH_ERROR_FINALIZING; case search_state::in_progress: case search_state::canceling: return CELL_SEARCH_ERROR_BUSY; default: return CELL_SEARCH_ERROR_GENERIC; } break; } return CELL_OK; } void populate_music_info(CellSearchMusicInfo& info, const utils::media_info& mi, const fs::dir_entry& item) { parse_metadata(info.artistName, mi, "artist", "Unknown Artist", CELL_SEARCH_TITLE_LEN_MAX); parse_metadata(info.albumTitle, mi, "album", "Unknown Album", CELL_SEARCH_TITLE_LEN_MAX); parse_metadata(info.genreName, mi, "genre", "Unknown Genre", CELL_SEARCH_TITLE_LEN_MAX); parse_metadata(info.title, mi, "title", item.name.substr(0, item.name.find_last_of('.')), CELL_SEARCH_TITLE_LEN_MAX); parse_metadata(info.diskNumber, mi, "disc", "1/1", sizeof(info.diskNumber) - 1); // Special case: track is usually stored as e.g. 2/11 const std::string tmp = mi.get_metadata("track", ""s); s64 value{}; if (tmp.empty() || !try_to_int64(&value, tmp.substr(0, tmp.find('/')).c_str(), s32{smin}, s32{smax})) { value = -1; } info.trackNumber = static_cast(value); info.size = item.size; info.releasedYear = static_cast(mi.get_metadata("date", -1)); info.duration = mi.duration_us / 1000; // we need microseconds info.samplingRate = mi.sample_rate; info.bitrate = mi.audio_bitrate_bps; info.quantizationBitrate = mi.audio_bitrate_bps; // TODO: Assumption, verify value info.playCount = 0; // we do not track this for now info.lastPlayedDate = -1; // we do not track this for now info.importedDate = -1; // we do not track this for now info.drmEncrypted = 0; // TODO: Needs to be 1 if it's encrypted info.status = CELL_SEARCH_CONTENTSTATUS_AVAILABLE; // Convert AVCodecID to CellSearchCodec switch (mi.audio_av_codec_id) { case 86017: // AV_CODEC_ID_MP3 info.codec = CELL_SEARCH_CODEC_MP3; break; case 86018: // AV_CODEC_ID_AAC info.codec = CELL_SEARCH_CODEC_AAC; break; case 86019: // AV_CODEC_ID_AC3 info.codec = CELL_SEARCH_CODEC_AC3; break; case 86023: // AV_CODEC_ID_WMAV1 case 86024: // AV_CODEC_ID_WMAV2 info.codec = CELL_SEARCH_CODEC_WMA; break; case 86047: // AV_CODEC_ID_ATRAC3 info.codec = CELL_SEARCH_CODEC_AT3; break; case 86055: // AV_CODEC_ID_ATRAC3P info.codec = CELL_SEARCH_CODEC_AT3P; break; case 88078: // AV_CODEC_ID_ATRAC3AL // case 88079: // AV_CODEC_ID_ATRAC3PAL TODO: supported ? info.codec = CELL_SEARCH_CODEC_ATALL; break; // TODO: Find out if any of this works // case 88069: // AV_CODEC_ID_DSD_LSBF // case 88070: // AV_CODEC_ID_DSD_MSBF // case 88071: // AV_CODEC_ID_DSD_LSBF_PLANAR // case 88072: // AV_CODEC_ID_DSD_MSBF_PLANAR // info.codec = CELL_SEARCH_CODEC_DSD; // break; // case ???: // info.codec = CELL_SEARCH_CODEC_WAV; // break; default: info.codec = CELL_SEARCH_CODEC_UNKNOWN; info.status = CELL_SEARCH_CONTENTSTATUS_NOT_SUPPORTED; break; } cellSearch.notice("CellSearchMusicInfo:, title=%s, albumTitle=%s, " "artistName=%s, genreName=%s, diskNumber=%s, " "trackNumber=%d, duration=%d, size=%d, importedDate=%d, " "lastPlayedDate=%d, releasedYear=%d, bitrate=%d, " "samplingRate=%d, quantizationBitrate=%d, playCount=%d, " "drmEncrypted=%d, codec=%d, status=%d", info.title, info.albumTitle, info.artistName, info.genreName, info.diskNumber, info.trackNumber, info.duration, info.size, info.importedDate, info.lastPlayedDate, info.releasedYear, info.bitrate, info.samplingRate, info.quantizationBitrate, info.playCount, info.drmEncrypted, info.codec, info.status); } void populate_video_info(CellSearchVideoInfo& info, const utils::media_info& mi, const fs::dir_entry& item) { parse_metadata(info.albumTitle, mi, "album", "Unknown Album", CELL_SEARCH_TITLE_LEN_MAX); parse_metadata(info.title, mi, "title", item.name.substr(0, item.name.find_last_of('.')), CELL_SEARCH_TITLE_LEN_MAX); info.size = item.size; info.duration = mi.duration_us / 1000; // we need microseconds info.audioBitrate = mi.audio_bitrate_bps; info.videoBitrate = mi.video_bitrate_bps; info.playCount = 0; // we do not track this for now info.importedDate = -1; // we do not track this for now info.takenDate = -1; // we do not track this for now info.drmEncrypted = 0; // TODO: Needs to be 1 if it's encrypted info.status = CELL_SEARCH_CONTENTSTATUS_AVAILABLE; // Convert Video AVCodecID to CellSearchCodec switch (mi.video_av_codec_id) { case 1: // AV_CODEC_ID_MPEG1VIDEO info.videoCodec = CELL_SEARCH_CODEC_MPEG1; break; case 2: // AV_CODEC_ID_MPEG2VIDEO info.videoCodec = CELL_SEARCH_CODEC_MPEG2; break; case 12: // AV_CODEC_ID_MPEG4 info.videoCodec = CELL_SEARCH_CODEC_MPEG4; break; case 27: // AV_CODEC_ID_H264 info.videoCodec = CELL_SEARCH_CODEC_AVC; break; default: info.videoCodec = CELL_SEARCH_CODEC_UNKNOWN; info.status = CELL_SEARCH_CONTENTSTATUS_NOT_SUPPORTED; break; } // Convert Audio AVCodecID to CellSearchCodec switch (mi.audio_av_codec_id) { // Let's ignore this due to CELL_SEARCH_CODEC_MPEG1_LAYER3 // case 86017: // AV_CODEC_ID_MP3 // info.audioCodec = CELL_SEARCH_CODEC_MP3; // break; case 86018: // AV_CODEC_ID_AAC info.audioCodec = CELL_SEARCH_CODEC_AAC; break; case 86019: // AV_CODEC_ID_AC3 info.audioCodec = CELL_SEARCH_CODEC_AC3; break; case 86023: // AV_CODEC_ID_WMAV1 case 86024: // AV_CODEC_ID_WMAV2 info.audioCodec = CELL_SEARCH_CODEC_WMA; break; case 86047: // AV_CODEC_ID_ATRAC3 info.audioCodec = CELL_SEARCH_CODEC_AT3; break; case 86055: // AV_CODEC_ID_ATRAC3P info.audioCodec = CELL_SEARCH_CODEC_AT3P; break; case 88078: // AV_CODEC_ID_ATRAC3AL // case 88079: // AV_CODEC_ID_ATRAC3PAL TODO: supported ? info.audioCodec = CELL_SEARCH_CODEC_ATALL; break; // TODO: Find out if any of this works // case 88069: // AV_CODEC_ID_DSD_LSBF // case 88070: // AV_CODEC_ID_DSD_MSBF // case 88071: // AV_CODEC_ID_DSD_LSBF_PLANAR // case 88072: // AV_CODEC_ID_DSD_MSBF_PLANAR // info.audioCodec = CELL_SEARCH_CODEC_DSD; // break; // case ???: // info.audioCodec = CELL_SEARCH_CODEC_WAV; // break; case 86058: // AV_CODEC_ID_MP1 info.audioCodec = CELL_SEARCH_CODEC_MPEG1_LAYER1; break; case 86016: // AV_CODEC_ID_MP2 info.audioCodec = CELL_SEARCH_CODEC_MPEG1_LAYER2; break; case 86017: // AV_CODEC_ID_MP3 info.audioCodec = CELL_SEARCH_CODEC_MPEG1_LAYER3; break; // case ???: // info.audioCodec = CELL_SEARCH_CODEC_MPEG2_LAYER1; // break; // case ???: // info.audioCodec = CELL_SEARCH_CODEC_MPEG2_LAYER2; // break; // case ???: // info.audioCodec = CELL_SEARCH_CODEC_MPEG2_LAYER3; // break; default: info.audioCodec = CELL_SEARCH_CODEC_UNKNOWN; info.status = CELL_SEARCH_CONTENTSTATUS_NOT_SUPPORTED; break; } cellSearch.notice("CellSearchVideoInfo: title='%s', albumTitle='%s', " "duration=%d, size=%d, importedDate=%d, takenDate=%d, " "videoBitrate=%d, audioBitrate=%d, playCount=%d, " "drmEncrypted=%d, videoCodec=%d, audioCodec=%d, status=%d", info.title, info.albumTitle, info.duration, info.size, info.importedDate, info.takenDate, info.videoBitrate, info.audioBitrate, info.playCount, info.drmEncrypted, info.videoCodec, info.audioCodec, info.status); } void populate_photo_info(CellSearchPhotoInfo& info, const utils::media_info& mi, const fs::dir_entry& item) { // TODO - Some kinda file photo analysis and assign the values as such info.size = item.size; info.importedDate = -1; info.takenDate = -1; info.width = mi.width; info.height = mi.height; info.orientation = mi.orientation; info.status = CELL_SEARCH_CONTENTSTATUS_AVAILABLE; strcpy_trunc(info.title, item.name.substr(0, item.name.find_last_of('.'))); strcpy_trunc(info.albumTitle, "ALBUM TITLE"); const std::string sub_type = fmt::to_lower(mi.sub_type); if (sub_type == "jpg" || sub_type == "jpeg") { info.codec = CELL_SEARCH_CODEC_JPEG; } else if (sub_type == "png") { info.codec = CELL_SEARCH_CODEC_PNG; } else if (sub_type == "tif" || sub_type == "tiff") { info.codec = CELL_SEARCH_CODEC_TIFF; } else if (sub_type == "bmp") { info.codec = CELL_SEARCH_CODEC_BMP; } else if (sub_type == "gif") { info.codec = CELL_SEARCH_CODEC_GIF; } else if (sub_type == "mpo") { info.codec = CELL_SEARCH_CODEC_MPO; } else { info.codec = CELL_SEARCH_CODEC_UNKNOWN; } cellSearch.notice("CellSearchPhotoInfo: title='%s', albumTitle='%s', " "size=%d, width=%d, height=%d, orientation=%d, codec=%d, " "status=%d, importedDate=%d, takenDate=%d", info.title, info.albumTitle, info.size, info.width, info.height, info.orientation, info.codec, info.status, info.importedDate, info.takenDate); } error_code cellSearchInitialize(CellSearchMode mode, u32 container, vm::ptr func, vm::ptr userData) { cellSearch.warning("cellSearchInitialize(mode=0x%x, container=0x%x, " "func=*0x%x, userData=*0x%x)", +mode, container, func, userData); if (mode != CELL_SEARCH_MODE_NORMAL) { return CELL_SEARCH_ERROR_UNKNOWN_MODE; } if (!func) { return CELL_SEARCH_ERROR_PARAM; } auto& search = g_fxo->get(); if (error_code error = check_search_state( search.state.compare_and_swap(search_state::not_initialized, search_state::initializing), search_state::initializing)) { return error; } search.func = func; search.userData = userData; sysutil_register_cb([=, &search](ppu_thread& ppu) -> s32 { search.state.store(search_state::idle); func(ppu, CELL_SEARCH_EVENT_INITIALIZE_RESULT, CELL_OK, vm::null, userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchFinalize() { cellSearch.todo("cellSearchFinalize()"); auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.compare_and_swap( search_state::idle, search_state::finalizing), search_state::finalizing)) { return error; } sysutil_register_cb([&search](ppu_thread& ppu) -> s32 { { std::lock_guard lock(search.links_mutex); search.content_links.clear(); } search.state.store(search_state::not_initialized); search.func(ppu, CELL_SEARCH_EVENT_FINALIZE_RESULT, CELL_OK, vm::null, search.userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchStartListSearch(CellSearchListSearchType type, CellSearchSortOrder sortOrder, vm::ptr outSearchId) { cellSearch.todo( "cellSearchStartListSearch(type=0x%x, sortOrder=0x%x, outSearchId=*0x%x)", +type, +sortOrder, outSearchId); if (!outSearchId) { return CELL_SEARCH_ERROR_PARAM; } // Reset values first *outSearchId = 0; const char* media_dir; switch (type) { case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_ALBUM: case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_GENRE: case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_ARTIST: case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_PLAYLIST: media_dir = "music"; break; case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_YEAR: case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_MONTH: case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_ALBUM: case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_PLAYLIST: media_dir = "photo"; break; case CELL_SEARCH_LISTSEARCHTYPE_VIDEO_ALBUM: media_dir = "video"; break; case CELL_SEARCH_LISTSEARCHTYPE_NONE: default: return CELL_SEARCH_ERROR_PARAM; } if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING && sortOrder != CELL_SEARCH_SORTORDER_DESCENDING) { return CELL_SEARCH_ERROR_PARAM; } if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING) { return CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH; } auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.compare_and_swap( search_state::idle, search_state::in_progress), search_state::in_progress)) { return error; } const u32 id = *outSearchId = idm::make(); sysutil_register_cb([=, &content_map = g_fxo->get(), &search](ppu_thread& ppu) -> s32 { auto curr_search = idm::get_unlocked(id); vm::var resultParam; resultParam->searchId = id; resultParam->resultNum = 0; // Set again later std::function searchInFolder = [&, type](const std::string& vpath) { // TODO: this is just a workaround. On a real PS3 the playlists seem // to be stored in dev_hdd0/mms/db/metadata_db_hdd std::vector dirs_sorted; for (auto&& entry : fs::dir(vfs::get(vpath))) { entry.name = vfs::unescape(entry.name); if (entry.is_directory) { if (entry.name == "." || entry.name == "..") { continue; // these dirs are not included in the dir list } dirs_sorted.push_back(entry); } } // clang-format off std::sort(dirs_sorted.begin(), dirs_sorted.end(), [&](const fs::dir_entry& a, const fs::dir_entry& b) -> bool { switch (sortOrder) { case CELL_SEARCH_SORTORDER_ASCENDING: // Order alphabetically ascending return a.name < b.name; case CELL_SEARCH_SORTORDER_DESCENDING: // Order alphabetically descending return a.name > b.name; default: { return false; } } }); // clang-format on for (auto&& item : dirs_sorted) { item.name = vfs::unescape(item.name); if (item.name == "." || item.name == ".." || !item.is_directory) { continue; } const std::string item_path(vpath + "/" + item.name); // Count files u32 numOfItems = 0; for (auto&& file : fs::dir(vfs::get(item_path))) { file.name = vfs::unescape(file.name); if (file.name == "." || file.name == ".." || file.is_directory) { continue; } numOfItems++; } const u64 hash = std::hash()(item_path); auto found = content_map.map.find(hash); if (found == content_map.map.end()) // content isn't yet being tracked { shared_ptr curr_find = make_shared(); if (item_path.length() > CELL_SEARCH_PATH_LEN_MAX) { // TODO: Create mapping which will be resolved to an actual hard // link in VFS by cellSearchPrepareFile cellSearch.warning( "cellSearchStartListSearch(): Directory-Path \"%s\" is too " "long and will be omitted: %i", item_path, item_path.length()); continue; // const size_t ext_offset = item.name.find_last_of('.'); // std::string link = link_base + std::to_string(hash) + // item.name.substr(ext_offset); // strcpy_trunc(curr_find->infoPath.contentPath, link); // std::lock_guard lock(search.links_mutex); // search.content_links.emplace(std::move(link), // search_info::link_data{ .path = item_path, .is_dir = true }); } else { strcpy_trunc(curr_find->infoPath.contentPath, item_path); } if (item.name.size() > CELL_SEARCH_TITLE_LEN_MAX) { item.name.resize(CELL_SEARCH_TITLE_LEN_MAX); } switch (type) { case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_ALBUM: case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_GENRE: case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_ARTIST: case CELL_SEARCH_LISTSEARCHTYPE_MUSIC_PLAYLIST: { curr_find->type = CELL_SEARCH_CONTENTTYPE_MUSICLIST; CellSearchMusicListInfo& info = curr_find->data.music_list; info.listType = type; // CellSearchListType matches CellSearchListSearchType info.numOfItems = numOfItems; info.duration = 0; strcpy_trunc(info.title, item.name); strcpy_trunc(info.artistName, "ARTIST NAME"); cellSearch.notice( "CellSearchMusicListInfo: title='%s', artistName='%s', " "listType=%d, numOfItems=%d, duration=%d", info.title, info.artistName, info.listType, info.numOfItems, info.duration); break; } case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_YEAR: case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_MONTH: case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_ALBUM: case CELL_SEARCH_LISTSEARCHTYPE_PHOTO_PLAYLIST: { curr_find->type = CELL_SEARCH_CONTENTTYPE_PHOTOLIST; CellSearchPhotoListInfo& info = curr_find->data.photo_list; info.listType = type; // CellSearchListType matches CellSearchListSearchType info.numOfItems = numOfItems; strcpy_trunc(info.title, item.name); cellSearch.notice("CellSearchPhotoListInfo: title='%s', " "listType=%d, numOfItems=%d", info.title, info.listType, info.numOfItems); break; } case CELL_SEARCH_LISTSEARCHTYPE_VIDEO_ALBUM: { curr_find->type = CELL_SEARCH_CONTENTTYPE_VIDEOLIST; CellSearchVideoListInfo& info = curr_find->data.video_list; info.listType = type; // CellSearchListType matches CellSearchListSearchType info.numOfItems = numOfItems; info.duration = 0; strcpy_trunc(info.title, item.name); cellSearch.notice("CellSearchVideoListInfo: title='%s', " "listType=%d, numOfItems=%d, duration=%d", info.title, info.listType, info.numOfItems, info.duration); break; } case CELL_SEARCH_LISTSEARCHTYPE_NONE: default: { // Should be unreachable, because it is already handled in the // main function break; } } content_map.map.emplace(hash, curr_find); curr_search->content_ids.emplace_back( hash, curr_find); // place this file's "ID" into the list of // found types cellSearch.notice("cellSearchStartListSearch(): CellSearchId: " "0x%x, Content ID: %08X, Path: \"%s\"", id, hash, item_path); } else // list is already stored and tracked { // TODO // Perform checks to see if the identified list has been modified // since last checked In which case, update the stored content's // properties auto content_found = &content_map->at(content_id); curr_search->content_ids.emplace_back(found->first, found->second); cellSearch.notice( "cellSearchStartListSearch(): Already tracked: CellSearchId: " "0x%x, Content ID: %08X, Path: \"%s\"", id, hash, item_path); } } }; searchInFolder(fmt::format("/dev_hdd0/%s", media_dir)); resultParam->resultNum = ::narrow(curr_search->content_ids.size()); search.state.store(search_state::idle); search.func(ppu, CELL_SEARCH_EVENT_LISTSEARCH_RESULT, CELL_OK, vm::cast(resultParam.addr()), search.userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchStartContentSearchInList( vm::cptr listId, CellSearchSortKey sortKey, CellSearchSortOrder sortOrder, vm::ptr outSearchId) { cellSearch.todo("cellSearchStartContentSearchInList(listId=*0x%x, " "sortKey=0x%x, sortOrder=0x%x, outSearchId=*0x%x)", listId, +sortKey, +sortOrder, outSearchId); // Reset values first if (outSearchId) { *outSearchId = 0; } if (!listId || !outSearchId) { return CELL_SEARCH_ERROR_PARAM; } switch (sortKey) { case CELL_SEARCH_SORTKEY_DEFAULT: case CELL_SEARCH_SORTKEY_TITLE: case CELL_SEARCH_SORTKEY_ALBUMTITLE: case CELL_SEARCH_SORTKEY_GENRENAME: case CELL_SEARCH_SORTKEY_ARTISTNAME: case CELL_SEARCH_SORTKEY_IMPORTEDDATE: case CELL_SEARCH_SORTKEY_TRACKNUMBER: case CELL_SEARCH_SORTKEY_TAKENDATE: case CELL_SEARCH_SORTKEY_USERDEFINED: case CELL_SEARCH_SORTKEY_MODIFIEDDATE: break; case CELL_SEARCH_SORTKEY_NONE: default: return CELL_SEARCH_ERROR_PARAM; } if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING && sortOrder != CELL_SEARCH_SORTORDER_DESCENDING) { return CELL_SEARCH_ERROR_PARAM; } auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.compare_and_swap( search_state::idle, search_state::in_progress), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(listId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } CellSearchContentSearchType type = CELL_SEARCH_CONTENTSEARCHTYPE_NONE; const auto& content_info = found->second; switch (content_info->type) { case CELL_SEARCH_CONTENTTYPE_MUSICLIST: type = CELL_SEARCH_CONTENTSEARCHTYPE_MUSIC_ALL; break; case CELL_SEARCH_CONTENTTYPE_PHOTOLIST: type = CELL_SEARCH_CONTENTSEARCHTYPE_PHOTO_ALL; break; case CELL_SEARCH_CONTENTTYPE_VIDEOLIST: type = CELL_SEARCH_CONTENTSEARCHTYPE_VIDEO_ALL; break; case CELL_SEARCH_CONTENTTYPE_MUSIC: case CELL_SEARCH_CONTENTTYPE_PHOTO: case CELL_SEARCH_CONTENTTYPE_VIDEO: case CELL_SEARCH_CONTENTTYPE_SCENE: case CELL_SEARCH_CONTENTTYPE_NONE: default: return CELL_SEARCH_ERROR_NOT_LIST; } const u32 id = *outSearchId = idm::make(); sysutil_register_cb([=, list_path = std::string(content_info->infoPath.contentPath), &search, &content_map](ppu_thread& ppu) -> s32 { auto curr_search = idm::get_unlocked(id); vm::var resultParam; resultParam->searchId = id; resultParam->resultNum = 0; // Set again later std::function searchInFolder = [&, type](const std::string& vpath) { std::vector files_sorted; for (auto&& entry : fs::dir(vfs::get(vpath))) { entry.name = vfs::unescape(entry.name); if (entry.is_directory || entry.name == "." || entry.name == "..") { continue; } files_sorted.push_back(entry); } // clang-format off std::sort(files_sorted.begin(), files_sorted.end(), [&](const fs::dir_entry& a, const fs::dir_entry& b) -> bool { switch (sortOrder) { case CELL_SEARCH_SORTORDER_ASCENDING: // Order alphabetically ascending return a.name < b.name; case CELL_SEARCH_SORTORDER_DESCENDING: // Order alphabetically descending return a.name > b.name; default: { return false; } } }); // clang-format on // TODO: Use sortKey (CellSearchSortKey) to allow for sorting by // category for (auto&& item : files_sorted) { // TODO // Perform first check that file is of desired type. For example, // don't wanna go identifying "AlbumArt.jpg" as an MP3. Hrm... // Postpone this thought. Do games perform their own checks? DIVA // ignores anything without the MP3 extension. const std::string item_path(vpath + "/" + item.name); const u64 hash = std::hash()(item_path); auto found = content_map.map.find(hash); if (found == content_map.map.end()) // content isn't yet being tracked { shared_ptr curr_find = make_shared(); if (item_path.length() > CELL_SEARCH_PATH_LEN_MAX) { // Create mapping which will be resolved to an actual hard link // in VFS by cellSearchPrepareFile const size_t ext_offset = item.name.find_last_of('.'); std::string link = link_base + std::to_string(hash) + item.name.substr(ext_offset); strcpy_trunc(curr_find->infoPath.contentPath, link); std::lock_guard lock(search.links_mutex); search.content_links.emplace( std::move(link), search_info::link_data{.path = item_path, .is_dir = false}); } else { strcpy_trunc(curr_find->infoPath.contentPath, item_path); } // TODO - curr_find.infoPath.thumbnailPath switch (type) { case CELL_SEARCH_CONTENTSEARCHTYPE_MUSIC_ALL: { curr_find->type = CELL_SEARCH_CONTENTTYPE_MUSIC; const std::string path = vfs::get(item_path); const auto [success, mi] = utils::get_media_info(path, 1); // AVMEDIA_TYPE_AUDIO if (!success) { continue; } populate_music_info(curr_find->data.music, mi, item); break; } case CELL_SEARCH_CONTENTSEARCHTYPE_PHOTO_ALL: { curr_find->type = CELL_SEARCH_CONTENTTYPE_PHOTO; populate_photo_info(curr_find->data.photo, {}, item); break; } case CELL_SEARCH_CONTENTSEARCHTYPE_VIDEO_ALL: { curr_find->type = CELL_SEARCH_CONTENTTYPE_VIDEO; const std::string path = vfs::get(item_path); const auto [success, mi] = utils::get_media_info(path, 0); // AVMEDIA_TYPE_VIDEO if (!success) { continue; } populate_video_info(curr_find->data.video, mi, item); break; } case CELL_SEARCH_CONTENTSEARCHTYPE_NONE: default: { // Should be unreachable, because it is already handled in the // main function break; } } content_map.map.emplace(hash, curr_find); curr_search->content_ids.emplace_back( hash, curr_find); // place this file's "ID" into the list of // found types cellSearch.notice( "cellSearchStartContentSearchInList(): CellSearchId: 0x%x, " "Content ID: %08X, Path: \"%s\"", id, hash, item_path); } else // file is already stored and tracked { // TODO // Perform checks to see if the identified file has been modified // since last checked In which case, update the stored content's // properties auto content_found = &content_map->at(content_id); curr_search->content_ids.emplace_back(found->first, found->second); cellSearch.notice( "cellSearchStartContentSearchInList(): Already Tracked: " "CellSearchId: 0x%x, Content ID: %08X, Path: \"%s\"", id, hash, item_path); } } }; searchInFolder(list_path); resultParam->resultNum = ::narrow(curr_search->content_ids.size()); search.state.store(search_state::idle); search.func(ppu, CELL_SEARCH_EVENT_CONTENTSEARCH_INLIST_RESULT, CELL_OK, vm::cast(resultParam.addr()), search.userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchStartContentSearch(CellSearchContentSearchType type, CellSearchSortKey sortKey, CellSearchSortOrder sortOrder, vm::ptr outSearchId) { cellSearch.todo("cellSearchStartContentSearch(type=0x%x, sortKey=0x%x, " "sortOrder=0x%x, outSearchId=*0x%x)", +type, +sortKey, +sortOrder, outSearchId); // Reset values first if (outSearchId) { *outSearchId = 0; } if (!outSearchId) { return CELL_SEARCH_ERROR_PARAM; } switch (sortKey) { case CELL_SEARCH_SORTKEY_DEFAULT: case CELL_SEARCH_SORTKEY_TITLE: case CELL_SEARCH_SORTKEY_ALBUMTITLE: case CELL_SEARCH_SORTKEY_GENRENAME: case CELL_SEARCH_SORTKEY_ARTISTNAME: case CELL_SEARCH_SORTKEY_IMPORTEDDATE: case CELL_SEARCH_SORTKEY_TRACKNUMBER: case CELL_SEARCH_SORTKEY_TAKENDATE: case CELL_SEARCH_SORTKEY_USERDEFINED: case CELL_SEARCH_SORTKEY_MODIFIEDDATE: break; case CELL_SEARCH_SORTKEY_NONE: default: return CELL_SEARCH_ERROR_PARAM; } if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING && sortOrder != CELL_SEARCH_SORTORDER_DESCENDING) { return CELL_SEARCH_ERROR_PARAM; } const char* media_dir; switch (type) { case CELL_SEARCH_CONTENTSEARCHTYPE_MUSIC_ALL: media_dir = "music"; break; case CELL_SEARCH_CONTENTSEARCHTYPE_PHOTO_ALL: media_dir = "photo"; break; case CELL_SEARCH_CONTENTSEARCHTYPE_VIDEO_ALL: media_dir = "video"; break; case CELL_SEARCH_CONTENTSEARCHTYPE_NONE: default: return CELL_SEARCH_ERROR_PARAM; } auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.compare_and_swap( search_state::idle, search_state::in_progress), search_state::in_progress)) { return error; } if (sortKey == CELL_SEARCH_SORTKEY_DEFAULT) { switch (type) { case CELL_SEARCH_CONTENTSEARCHTYPE_MUSIC_ALL: sortKey = CELL_SEARCH_SORTKEY_ARTISTNAME; break; case CELL_SEARCH_CONTENTSEARCHTYPE_PHOTO_ALL: sortKey = CELL_SEARCH_SORTKEY_TAKENDATE; break; case CELL_SEARCH_CONTENTSEARCHTYPE_VIDEO_ALL: sortKey = CELL_SEARCH_SORTKEY_TITLE; break; default: break; } } if (sortKey != CELL_SEARCH_SORTKEY_IMPORTEDDATE && sortKey != CELL_SEARCH_SORTKEY_MODIFIEDDATE) { switch (type) { case CELL_SEARCH_CONTENTSEARCHTYPE_MUSIC_ALL: { if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING) { return CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH; } if (sortKey != CELL_SEARCH_SORTKEY_ARTISTNAME && sortKey != CELL_SEARCH_SORTKEY_ALBUMTITLE && sortKey != CELL_SEARCH_SORTKEY_GENRENAME && sortKey != CELL_SEARCH_SORTKEY_TITLE) { return CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH; } break; } case CELL_SEARCH_CONTENTSEARCHTYPE_PHOTO_ALL: { if (sortKey != CELL_SEARCH_SORTKEY_TAKENDATE) { if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING || sortKey != CELL_SEARCH_SORTKEY_TITLE) { return CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH; } } break; } case CELL_SEARCH_CONTENTSEARCHTYPE_VIDEO_ALL: { if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING || sortKey != CELL_SEARCH_SORTKEY_TITLE) { return CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH; } break; } default: break; } } const u32 id = *outSearchId = idm::make(); sysutil_register_cb([=, &content_map = g_fxo->get(), &search](ppu_thread& ppu) -> s32 { auto curr_search = idm::get_unlocked(id); vm::var resultParam; resultParam->searchId = id; resultParam->resultNum = 0; // Set again later std::function searchInFolder = [&, type](const std::string& vpath, const std::string& prev) { const std::string relative_vpath = (!prev.empty() ? prev + "/" : "") + vpath; for (auto&& item : fs::dir(vfs::get(relative_vpath))) { item.name = vfs::unescape(item.name); if (item.name == "." || item.name == "..") { continue; } if (item.is_directory) { searchInFolder(item.name, relative_vpath); continue; } // TODO // Perform first check that file is of desired type. For example, // don't wanna go identifying "AlbumArt.jpg" as an MP3. Hrm... // Postpone this thought. Do games perform their own checks? DIVA // ignores anything without the MP3 extension. // TODO - Identify sorting method and insert the appropriate values // where applicable const std::string item_path(relative_vpath + "/" + item.name); const u64 hash = std::hash()(item_path); auto found = content_map.map.find(hash); if (found == content_map.map.end()) // content isn't yet being tracked { shared_ptr curr_find = make_shared(); if (item_path.length() > CELL_SEARCH_PATH_LEN_MAX) { // Create mapping which will be resolved to an actual hard link // in VFS by cellSearchPrepareFile const size_t ext_offset = item.name.find_last_of('.'); std::string link = link_base + std::to_string(hash) + item.name.substr(ext_offset); strcpy_trunc(curr_find->infoPath.contentPath, link); std::lock_guard lock(search.links_mutex); search.content_links.emplace( std::move(link), search_info::link_data{.path = item_path, .is_dir = false}); } else { strcpy_trunc(curr_find->infoPath.contentPath, item_path); } // TODO - curr_find.infoPath.thumbnailPath switch (type) { case CELL_SEARCH_CONTENTSEARCHTYPE_MUSIC_ALL: { curr_find->type = CELL_SEARCH_CONTENTTYPE_MUSIC; const std::string path = vfs::get(item_path); const auto [success, mi] = utils::get_media_info(path, 1); // AVMEDIA_TYPE_AUDIO if (!success) { continue; } populate_music_info(curr_find->data.music, mi, item); break; } case CELL_SEARCH_CONTENTSEARCHTYPE_PHOTO_ALL: { curr_find->type = CELL_SEARCH_CONTENTTYPE_PHOTO; populate_photo_info(curr_find->data.photo, {}, item); break; } case CELL_SEARCH_CONTENTSEARCHTYPE_VIDEO_ALL: { curr_find->type = CELL_SEARCH_CONTENTTYPE_VIDEO; const std::string path = vfs::get(item_path); const auto [success, mi] = utils::get_media_info(path, 0); // AVMEDIA_TYPE_VIDEO if (!success) { continue; } populate_video_info(curr_find->data.video, mi, item); break; } case CELL_SEARCH_CONTENTSEARCHTYPE_NONE: default: { // Should be unreachable, because it is already handled in the // main function break; } } content_map.map.emplace(hash, curr_find); curr_search->content_ids.emplace_back( hash, curr_find); // place this file's "ID" into the list of // found types cellSearch.notice("cellSearchStartContentSearch(): CellSearchId: " "0x%x, Content ID: %08X, Path: \"%s\"", id, hash, item_path); } else // file is already stored and tracked { // TODO // Perform checks to see if the identified file has been modified // since last checked In which case, update the stored content's // properties auto content_found = &content_map->at(content_id); curr_search->content_ids.emplace_back(found->first, found->second); cellSearch.notice( "cellSearchStartContentSearch(): Already Tracked: " "CellSearchId: 0x%x, Content ID: %08X, Path: \"%s\"", id, hash, item_path); } } }; searchInFolder(fmt::format("/dev_hdd0/%s", media_dir), ""); resultParam->resultNum = ::narrow(curr_search->content_ids.size()); search.state.store(search_state::idle); search.func(ppu, CELL_SEARCH_EVENT_CONTENTSEARCH_RESULT, CELL_OK, vm::cast(resultParam.addr()), search.userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchStartSceneSearchInVideo( vm::cptr videoId, CellSearchSceneSearchType searchType, CellSearchSortOrder sortOrder, vm::ptr outSearchId) { cellSearch.todo("cellSearchStartSceneSearchInVideo(videoId=*0x%x, " "searchType=0x%x, sortOrder=0x%x, outSearchId=*0x%x)", videoId, +searchType, +sortOrder, outSearchId); // Reset values first if (outSearchId) { *outSearchId = 0; } if (!videoId || !outSearchId) { return CELL_SEARCH_ERROR_PARAM; } switch (searchType) { case CELL_SEARCH_SCENESEARCHTYPE_CHAPTER: case CELL_SEARCH_SCENESEARCHTYPE_CLIP_HIGHLIGHT: case CELL_SEARCH_SCENESEARCHTYPE_CLIP_USER: case CELL_SEARCH_SCENESEARCHTYPE_CLIP: case CELL_SEARCH_SCENESEARCHTYPE_ALL: break; case CELL_SEARCH_SCENESEARCHTYPE_NONE: default: return CELL_SEARCH_ERROR_PARAM; } if (sortOrder != CELL_SEARCH_SORTORDER_ASCENDING && sortOrder != CELL_SEARCH_SORTORDER_DESCENDING) { return CELL_SEARCH_ERROR_PARAM; } auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.compare_and_swap( search_state::idle, search_state::in_progress), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(videoId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } const auto& content_info = found->second; if (content_info->type != CELL_SEARCH_CONTENTTYPE_VIDEO) { return CELL_SEARCH_ERROR_INVALID_CONTENTTYPE; } const u32 id = *outSearchId = idm::make(); sysutil_register_cb([=, &search](ppu_thread& ppu) -> s32 { vm::var resultParam; resultParam->searchId = id; resultParam->resultNum = 0; // TODO search.state.store(search_state::idle); search.func(ppu, CELL_SEARCH_EVENT_SCENESEARCH_INVIDEO_RESULT, CELL_OK, vm::cast(resultParam.addr()), search.userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchStartSceneSearch(CellSearchSceneSearchType searchType, vm::cptr gameTitle, vm::cpptr tags, u32 tagNum, CellSearchSortKey sortKey, CellSearchSortOrder sortOrder, vm::ptr outSearchId) { cellSearch.todo( "cellSearchStartSceneSearch(searchType=0x%x, gameTitle=%s, tags=**0x%x, " "tagNum=0x%x, sortKey=0x%x, sortOrder=0x%x, outSearchId=*0x%x)", +searchType, gameTitle, tags, tagNum, +sortKey, +sortOrder, outSearchId); // Reset values first if (outSearchId) { *outSearchId = 0; } if (!gameTitle || !outSearchId) { return CELL_SEARCH_ERROR_PARAM; } switch (searchType) { case CELL_SEARCH_SCENESEARCHTYPE_CHAPTER: case CELL_SEARCH_SCENESEARCHTYPE_CLIP_HIGHLIGHT: case CELL_SEARCH_SCENESEARCHTYPE_CLIP_USER: case CELL_SEARCH_SCENESEARCHTYPE_CLIP: case CELL_SEARCH_SCENESEARCHTYPE_ALL: break; case CELL_SEARCH_SCENESEARCHTYPE_NONE: default: return CELL_SEARCH_ERROR_PARAM; } switch (sortKey) { case CELL_SEARCH_SORTKEY_DEFAULT: case CELL_SEARCH_SORTKEY_TITLE: case CELL_SEARCH_SORTKEY_ALBUMTITLE: case CELL_SEARCH_SORTKEY_GENRENAME: case CELL_SEARCH_SORTKEY_ARTISTNAME: case CELL_SEARCH_SORTKEY_IMPORTEDDATE: case CELL_SEARCH_SORTKEY_TRACKNUMBER: case CELL_SEARCH_SORTKEY_TAKENDATE: case CELL_SEARCH_SORTKEY_USERDEFINED: case CELL_SEARCH_SORTKEY_MODIFIEDDATE: break; case CELL_SEARCH_SORTKEY_NONE: default: return CELL_SEARCH_ERROR_PARAM; } if (tagNum) // TODO: find out if this is the correct location for these checks { if (tagNum > CELL_SEARCH_TAG_NUM_MAX || !tags) { return CELL_SEARCH_ERROR_TAG; } for (u32 n = 0; n < tagNum; n++) { if (!tags[tagNum] || !memchr(&tags[tagNum], '\0', CELL_SEARCH_TAG_LEN_MAX)) { return CELL_SEARCH_ERROR_TAG; } } } if (sortKey != CELL_SEARCH_SORTKEY_DEFAULT && sortKey != CELL_SEARCH_SORTKEY_IMPORTEDDATE && sortKey != CELL_SEARCH_SORTKEY_MODIFIEDDATE && (sortKey != CELL_SEARCH_SORTKEY_TITLE || sortOrder != CELL_SEARCH_SORTORDER_ASCENDING)) { return CELL_SEARCH_ERROR_NOT_SUPPORTED_SEARCH; } auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.compare_and_swap( search_state::idle, search_state::in_progress), search_state::in_progress)) { return error; } const u32 id = *outSearchId = idm::make(); sysutil_register_cb([=, &search](ppu_thread& ppu) -> s32 { vm::var resultParam; resultParam->searchId = id; resultParam->resultNum = 0; // TODO search.state.store(search_state::idle); search.func(ppu, CELL_SEARCH_EVENT_SCENESEARCH_RESULT, CELL_OK, vm::cast(resultParam.addr()), search.userData); return CELL_OK; }); return CELL_OK; } error_code cellSearchGetContentInfoByOffset(CellSearchId searchId, s32 offset, vm::ptr infoBuffer, vm::ptr outContentType, vm::ptr outContentId) { cellSearch.warning( "cellSearchGetContentInfoByOffset(searchId=0x%x, offset=0x%x, " "infoBuffer=*0x%x, outContentType=*0x%x, outContentId=*0x%x)", searchId, offset, infoBuffer, outContentType, outContentId); // Reset values first if (outContentType) { *outContentType = CELL_SEARCH_CONTENTTYPE_NONE; } if (infoBuffer) { std::memset(infoBuffer.get_ptr(), 0, CELL_SEARCH_CONTENT_BUFFER_SIZE_MAX); } if (outContentId) { std::memset(outContentId->data, 0, 4); std::memset(outContentId->data + 4, -1, CELL_SEARCH_CONTENT_ID_SIZE - 4); } const auto searchObject = idm::get_unlocked(searchId); if (!searchObject) { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } if (!outContentType || (!outContentId && !infoBuffer)) { return CELL_SEARCH_ERROR_PARAM; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } if (offset >= 0 && offset + 0u < searchObject->content_ids.size()) { const auto& content_id = searchObject->content_ids[offset]; const auto& content_info = content_id.second; switch (content_info->type) { case CELL_SEARCH_CONTENTTYPE_MUSIC: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.music, sizeof(content_info->data.music)); break; case CELL_SEARCH_CONTENTTYPE_PHOTO: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.photo, sizeof(content_info->data.photo)); break; case CELL_SEARCH_CONTENTTYPE_VIDEO: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.video, sizeof(content_info->data.photo)); break; case CELL_SEARCH_CONTENTTYPE_MUSICLIST: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.music_list, sizeof(content_info->data.music_list)); break; case CELL_SEARCH_CONTENTTYPE_PHOTOLIST: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.photo_list, sizeof(content_info->data.photo_list)); break; case CELL_SEARCH_CONTENTTYPE_VIDEOLIST: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.video_list, sizeof(content_info->data.video_list)); break; case CELL_SEARCH_CONTENTTYPE_SCENE: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.scene, sizeof(content_info->data.scene)); break; default: return CELL_SEARCH_ERROR_GENERIC; } const u128 content_id_128 = content_id.first; *outContentType = content_info->type; if (outContentId) { std::memcpy(outContentId->data, &content_id_128, CELL_SEARCH_CONTENT_ID_SIZE); } } else // content ID not found, perform a search first { return CELL_SEARCH_ERROR_OUT_OF_RANGE; } return CELL_OK; } error_code cellSearchGetContentInfoByContentId( vm::cptr contentId, vm::ptr infoBuffer, vm::ptr outContentType) { cellSearch.warning("cellSearchGetContentInfoByContentId(contentId=*0x%x, " "infoBuffer=*0x%x, outContentType=*0x%x)", contentId, infoBuffer, outContentType); // Reset values first if (outContentType) { *outContentType = CELL_SEARCH_CONTENTTYPE_NONE; } if (infoBuffer) { std::memset(infoBuffer.get_ptr(), 0, CELL_SEARCH_CONTENT_BUFFER_SIZE_MAX); } if (!outContentType || !contentId) { return CELL_SEARCH_ERROR_PARAM; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(contentId->data)); if (found != content_map.map.end()) { const auto& content_info = found->second; switch (content_info->type) { case CELL_SEARCH_CONTENTTYPE_MUSIC: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.music, sizeof(content_info->data.music)); break; case CELL_SEARCH_CONTENTTYPE_PHOTO: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.photo, sizeof(content_info->data.photo)); break; case CELL_SEARCH_CONTENTTYPE_VIDEO: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.video, sizeof(content_info->data.photo)); break; case CELL_SEARCH_CONTENTTYPE_MUSICLIST: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.music_list, sizeof(content_info->data.music_list)); break; case CELL_SEARCH_CONTENTTYPE_PHOTOLIST: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.photo_list, sizeof(content_info->data.photo_list)); break; case CELL_SEARCH_CONTENTTYPE_VIDEOLIST: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.video_list, sizeof(content_info->data.video_list)); break; case CELL_SEARCH_CONTENTTYPE_SCENE: if (infoBuffer) std::memcpy(infoBuffer.get_ptr(), &content_info->data.scene, sizeof(content_info->data.scene)); break; default: return CELL_SEARCH_ERROR_GENERIC; } *outContentType = content_info->type; } else // content ID not found, perform a search first { return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } return CELL_OK; } error_code cellSearchGetOffsetByContentId(CellSearchId searchId, vm::cptr contentId, vm::ptr outOffset) { cellSearch.warning("cellSearchGetOffsetByContentId(searchId=0x%x, " "contentId=*0x%x, outOffset=*0x%x)", searchId, contentId, outOffset); if (outOffset) { *outOffset = -1; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } const auto searchObject = idm::get_unlocked(searchId); if (!searchObject) { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } if (!outOffset || !contentId) { return CELL_SEARCH_ERROR_PARAM; } s32 i = 0; const u64 content_hash = *reinterpret_cast(contentId->data); for (auto& content_id : searchObject->content_ids) { if (content_id.first == content_hash) { *outOffset = i; return CELL_OK; } ++i; } return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } error_code cellSearchGetContentIdByOffset(CellSearchId searchId, s32 offset, vm::ptr outContentType, vm::ptr outContentId, vm::ptr outTimeInfo) { cellSearch.todo( "cellSearchGetContentIdByOffset(searchId=0x%x, offset=0x%x, " "outContentType=*0x%x, outContentId=*0x%x, outTimeInfo=*0x%x)", searchId, offset, outContentType, outContentId, outTimeInfo); // Reset values first if (outTimeInfo) { outTimeInfo->modifiedDate = -1; outTimeInfo->takenDate = -1; outTimeInfo->importedDate = -1; } if (outContentType) { *outContentType = CELL_SEARCH_CONTENTTYPE_NONE; } if (outContentId) { std::memset(outContentId->data, 0, 4); std::memset(outContentId->data + 4, -1, CELL_SEARCH_CONTENT_ID_SIZE - 4); } const auto searchObject = idm::get_unlocked(searchId); if (!searchObject) { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } if (!outContentType || !outContentId) { return CELL_SEARCH_ERROR_PARAM; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } if (offset >= 0 && offset + 0u < searchObject->content_ids.size()) { auto& content_id = ::at32(searchObject->content_ids, offset); const u128 content_id_128 = content_id.first; *outContentType = content_id.second->type; std::memcpy(outContentId->data, &content_id_128, CELL_SEARCH_CONTENT_ID_SIZE); if (outTimeInfo) { std::memcpy(outTimeInfo.get_ptr(), &content_id.second->time_info, sizeof(content_id.second->time_info)); } } else // content ID not found, perform a search first { return CELL_SEARCH_ERROR_OUT_OF_RANGE; } return CELL_OK; } error_code cellSearchGetContentInfoGameComment(vm::cptr contentId, vm::ptr gameComment) { cellSearch.todo( "cellSearchGetContentInfoGameComment(contentId=*0x%x, gameComment=*0x%x)", contentId, gameComment); // Reset values first if (gameComment) { gameComment[0] = 0; } if (!contentId || !gameComment) { return CELL_SEARCH_ERROR_PARAM; } // TODO: find out if this check is correct if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(contentId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } const auto& content_info = found->second; switch (content_info->type) { case CELL_SEARCH_CONTENTTYPE_MUSIC: case CELL_SEARCH_CONTENTTYPE_PHOTO: case CELL_SEARCH_CONTENTTYPE_VIDEO: break; default: return CELL_SEARCH_ERROR_INVALID_CONTENTTYPE; } // TODO: retrieve gameComment return CELL_OK; } error_code cellSearchGetMusicSelectionContext( CellSearchId searchId, vm::cptr contentId, CellSearchRepeatMode repeatMode, CellSearchContextOption option, vm::ptr outContext) { cellSearch.todo( "cellSearchGetMusicSelectionContext(searchId=0x%x, contentId=*0x%x, " "repeatMode=0x%x, option=0x%x, outContext=*0x%x)", searchId, contentId, +repeatMode, +option, outContext); if (!outContext) { return CELL_SEARCH_ERROR_PARAM; } // Reset values first std::memset(outContext->data, 0, 4); const auto searchObject = idm::get_unlocked(searchId); if (!searchObject) { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } auto& search = g_fxo->get(); // TODO: find out if this check is correct if (error_code error = check_search_state(search.state.load(), search_state::in_progress)) { return error; } if (searchObject->content_ids.empty()) { return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } music_selection_context context{}; // Use the first track in order to get info about this search const auto& first_content_id = searchObject->content_ids[0]; const auto& first_content = first_content_id.second; ensure(first_content); const auto get_random_content = [&searchObject, &first_content]() -> shared_ptr { if (searchObject->content_ids.size() == 1) { return first_content; } std::vector result; std::sample(searchObject->content_ids.begin(), searchObject->content_ids.end(), std::back_inserter(result), 1, std::mt19937{std::random_device{}()}); ensure(result.size() == 1); shared_ptr content = ensure(result[0].second); return content; }; if (contentId) { // Try to find the specified content const u64 content_hash = *reinterpret_cast(contentId->data); auto content = std::find_if(searchObject->content_ids.begin(), searchObject->content_ids.end(), [&content_hash](const content_id_type& cid) { return cid.first == content_hash; }); if (content != searchObject->content_ids.cend() && content->second) { // Check if the type of the found content is correct if (content->second->type != CELL_SEARCH_CONTENTTYPE_MUSIC) { return {CELL_SEARCH_ERROR_INVALID_CONTENTTYPE, "Type: %d, Expected: CELL_SEARCH_CONTENTTYPE_MUSIC"}; } // Check if the type of the found content matches our search content type if (content->second->type != first_content->type) { return {CELL_SEARCH_ERROR_NOT_SUPPORTED_CONTEXT, "Type: %d, Expected: %d", +content->second->type, +first_content->type}; } // Use the found content context.playlist.push_back(content->second->infoPath.contentPath); cellSearch.notice("cellSearchGetMusicSelectionContext(): Hash=%08X, " "Assigning found track: Type=0x%x, Path=%s", content_hash, +content->second->type, context.playlist.back()); } else if (first_content->type == CELL_SEARCH_CONTENTTYPE_MUSICLIST) { // Abort if we can't find the playlist. return {CELL_SEARCH_ERROR_CONTENT_NOT_FOUND, "Type: CELL_SEARCH_CONTENTTYPE_MUSICLIST"}; } else if (option == CELL_SEARCH_CONTEXTOPTION_SHUFFLE) { // Select random track // TODO: whole playlist shared_ptr content = get_random_content(); context.playlist.push_back(content->infoPath.contentPath); cellSearch.notice("cellSearchGetMusicSelectionContext(): Hash=%08X, " "Assigning random track: Type=0x%x, Path=%s", content_hash, +content->type, context.playlist.back()); } else { // Select the first track by default // TODO: whole playlist context.playlist.push_back(first_content->infoPath.contentPath); cellSearch.notice("cellSearchGetMusicSelectionContext(): Hash=%08X, " "Assigning first track: Type=0x%x, Path=%s", content_hash, +first_content->type, context.playlist.back()); } } else if (first_content->type == CELL_SEARCH_CONTENTTYPE_MUSICLIST) { // Abort if we don't have the necessary info to select a playlist. return {CELL_SEARCH_ERROR_NOT_SUPPORTED_CONTEXT, "Type: CELL_SEARCH_CONTENTTYPE_MUSICLIST"}; } else if (option == CELL_SEARCH_CONTEXTOPTION_SHUFFLE) { // Select random track // TODO: whole playlist shared_ptr content = get_random_content(); context.playlist.push_back(content->infoPath.contentPath); cellSearch.notice("cellSearchGetMusicSelectionContext(): Assigning random " "track: Type=0x%x, Path=%s", +content->type, context.playlist.back()); } else { // Select the first track by default // TODO: whole playlist context.playlist.push_back(first_content->infoPath.contentPath); cellSearch.notice("cellSearchGetMusicSelectionContext(): Assigning first " "track: Type=0x%x, Path=%s", +first_content->type, context.playlist.back()); } context.content_type = first_content->type; context.repeat_mode = repeatMode; context.context_option = option; // TODO: context.first_track = ?; // Resolve hashed paths for (std::string& track : context.playlist) { if (auto found = search.content_links.find(track); found != search.content_links.end()) { track = found->second.path; } } context.create_playlist(music_selection_context::get_next_hash()); *outContext = context.get(); cellSearch.success( "cellSearchGetMusicSelectionContext: found selection context: %d", context.to_string()); return CELL_OK; } error_code cellSearchGetMusicSelectionContextOfSingleTrack( vm::cptr contentId, vm::ptr outContext) { cellSearch.todo("cellSearchGetMusicSelectionContextOfSingleTrack(contentId=*" "0x%x, outContext=*0x%x)", contentId, outContext); // Reset values first if (outContext) { std::memset(outContext->data, 0, 4); } if (!contentId || !outContext) { return CELL_SEARCH_ERROR_PARAM; } auto& search = g_fxo->get(); // TODO: find out if this check is correct if (error_code error = check_search_state(search.state.load(), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(contentId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } const auto& content_info = found->second; if (content_info->type != CELL_SEARCH_CONTENTTYPE_MUSIC) { return CELL_SEARCH_ERROR_INVALID_CONTENTTYPE; } music_selection_context context{}; context.playlist.push_back(content_info->infoPath.contentPath); context.repeat_mode = content_info->repeat_mode; context.context_option = content_info->context_option; // Resolve hashed paths for (std::string& track : context.playlist) { if (auto found = search.content_links.find(track); found != search.content_links.end()) { track = found->second.path; } } context.create_playlist(music_selection_context::get_next_hash()); *outContext = context.get(); cellSearch.success("cellSearchGetMusicSelectionContextOfSingleTrack: found " "selection context: %s", context.to_string()); return CELL_OK; } error_code cellSearchGetContentInfoPath(vm::cptr contentId, vm::ptr infoPath) { cellSearch.todo( "cellSearchGetContentInfoPath(contentId=*0x%x, infoPath=*0x%x)", contentId, infoPath); // Reset values first if (infoPath) { *infoPath = {}; } if (!contentId || !infoPath) { return CELL_SEARCH_ERROR_PARAM; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } const u64 id = *reinterpret_cast(contentId->data); auto& content_map = g_fxo->get(); auto found = content_map.map.find(id); if (found != content_map.map.end()) { std::memcpy(infoPath.get_ptr(), &found->second->infoPath, sizeof(found->second->infoPath)); } else { cellSearch.error("cellSearchGetContentInfoPath(): ID not found : 0x%08X", id); return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } cellSearch.success("contentId=%08X, contentPath=\"%s\"", id, infoPath->contentPath); return CELL_OK; } error_code cellSearchGetContentInfoPathMovieThumb( vm::cptr contentId, vm::ptr infoMt) { cellSearch.todo( "cellSearchGetContentInfoPathMovieThumb(contentId=*0x%x, infoMt=*0x%x)", contentId, infoMt); // Reset values first if (infoMt) { *infoMt = {}; } if (!contentId || !infoMt) { return CELL_SEARCH_ERROR_PARAM; } // TODO: find out if this check is correct if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(contentId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } const auto& content_info = found->second; if (content_info->type != CELL_SEARCH_CONTENTTYPE_VIDEO) { return CELL_SEARCH_ERROR_INVALID_CONTENTTYPE; } strcpy_trunc(infoMt->movieThumbnailPath, content_info->infoPath.thumbnailPath); // TODO: set infoMt->movieThumbnailOption return CELL_OK; } error_code cellSearchPrepareFile(vm::cptr path) { cellSearch.todo("cellSearchPrepareFile(path=%s)", path); if (!path) { return CELL_SEARCH_ERROR_PARAM; } auto& search = g_fxo->get(); if (error_code error = check_search_state(search.state.load(), search_state::in_progress)) { return error; } reader_lock lock(search.links_mutex); auto found = search.content_links.find(path.get_ptr()); if (found != search.content_links.end()) { vfs::mount(found->first, vfs::get(found->second.path), found->second.is_dir); } return CELL_OK; } error_code cellSearchGetContentInfoDeveloperData(vm::cptr contentId, vm::ptr developerData) { cellSearch.todo("cellSearchGetContentInfoDeveloperData(contentId=*0x%x, " "developerData=*0x%x)", contentId, developerData); // Reset values first if (developerData) { developerData[0] = 0; } if (!contentId || !developerData) { return CELL_SEARCH_ERROR_PARAM; } // TODO: find out if this check is correct if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(contentId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } const auto& content_info = found->second; switch (content_info->type) { case CELL_SEARCH_CONTENTTYPE_VIDEO: case CELL_SEARCH_CONTENTTYPE_SCENE: break; default: return CELL_SEARCH_ERROR_INVALID_CONTENTTYPE; } // TODO: retrieve developerData return CELL_OK; } error_code cellSearchGetContentInfoSharable(vm::cptr contentId, vm::ptr sharable) { cellSearch.todo( "cellSearchGetContentInfoSharable(contentId=*0x%x, sharable=*0x%x)", contentId, sharable); // Reset values first if (sharable) { *sharable = CELL_SEARCH_SHARABLETYPE_PROHIBITED; } if (!contentId || !sharable) { return CELL_SEARCH_ERROR_PARAM; } // TODO: find out if this check is correct if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::in_progress)) { return error; } auto& content_map = g_fxo->get(); auto found = content_map.map.find(*reinterpret_cast(contentId->data)); if (found == content_map.map.end()) { // content ID not found, perform a search first return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; } const auto& content_info = found->second; if (content_info->type != CELL_SEARCH_CONTENTTYPE_VIDEO) { return CELL_SEARCH_ERROR_INVALID_CONTENTTYPE; } // TODO: retrieve sharable *sharable = CELL_SEARCH_SHARABLETYPE_PROHIBITED; return CELL_OK; } error_code cellSearchCancel(CellSearchId searchId) { cellSearch.todo("cellSearchCancel(searchId=0x%x)", searchId); const auto searchObject = idm::get_unlocked(searchId); if (!searchObject) { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::canceling)) { return error; } // TODO return CELL_OK; } error_code cellSearchEnd(CellSearchId searchId) { cellSearch.todo("cellSearchEnd(searchId=0x%x)", searchId); if (!searchId) // This check has to come first { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } if (error_code error = check_search_state( g_fxo->get().state.load(), search_state::finalizing)) { return error; } const auto searchObject = idm::get_unlocked(searchId); if (!searchObject) { return CELL_SEARCH_ERROR_INVALID_SEARCHID; } idm::remove(searchId); return CELL_OK; } DECLARE(ppu_module_manager::cellSearch)("cellSearchUtility", []() { REG_FUNC(cellSearchUtility, cellSearchInitialize); REG_FUNC(cellSearchUtility, cellSearchFinalize); REG_FUNC(cellSearchUtility, cellSearchStartListSearch); REG_FUNC(cellSearchUtility, cellSearchStartContentSearchInList); REG_FUNC(cellSearchUtility, cellSearchStartContentSearch); REG_FUNC(cellSearchUtility, cellSearchStartSceneSearchInVideo); REG_FUNC(cellSearchUtility, cellSearchStartSceneSearch); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoByOffset); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoByContentId); REG_FUNC(cellSearchUtility, cellSearchGetOffsetByContentId); REG_FUNC(cellSearchUtility, cellSearchGetContentIdByOffset); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoGameComment); REG_FUNC(cellSearchUtility, cellSearchGetMusicSelectionContext); REG_FUNC(cellSearchUtility, cellSearchGetMusicSelectionContextOfSingleTrack); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoPath); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoPathMovieThumb); REG_FUNC(cellSearchUtility, cellSearchPrepareFile); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoDeveloperData); REG_FUNC(cellSearchUtility, cellSearchGetContentInfoSharable); REG_FUNC(cellSearchUtility, cellSearchCancel); REG_FUNC(cellSearchUtility, cellSearchEnd); }); // Helper error_code music_selection_context::find_content_id( vm::ptr contents_id) { if (!contents_id) return CELL_MUSIC_ERROR_PARAM; // Search for the content that matches our current selection auto& content_map = g_fxo->get(); shared_ptr found_content; u64 hash = 0; for (const std::string& track : playlist) { if (content_type == CELL_SEARCH_CONTENTTYPE_MUSICLIST) { hash = std::hash()(fs::get_parent_dir(track)); } else { hash = std::hash()(track); } if (auto found = content_map.map.find(hash); found != content_map.map.end()) { found_content = found->second; break; } } if (found_content) { // TODO: check if the content type is correct const u128 content_id_128 = hash; std::memcpy(contents_id->data, &content_id_128, CELL_SEARCH_CONTENT_ID_SIZE); cellSearch.warning( "find_content_id: found existing content for %s (path control: '%s')", to_string(), found_content->infoPath.contentPath); return CELL_OK; } // Try to find the content manually auto& search = g_fxo->get(); const std::string music_dir = "/dev_hdd0/music/"; const std::string vfs_music_dir = vfs::get(music_dir); for (auto&& entry : fs::dir(vfs_music_dir)) { entry.name = vfs::unescape(entry.name); if (!entry.is_directory || entry.name == "." || entry.name == "..") { continue; } const std::string dir_path = music_dir + entry.name; const std::string vfs_dir_path = vfs_music_dir + entry.name; if (content_type == CELL_SEARCH_CONTENTTYPE_MUSICLIST) { const u64 dir_hash = std::hash()(dir_path); if (hash == dir_hash) { u32 num_of_items = 0; for (auto&& file : fs::dir(vfs_dir_path)) { file.name = vfs::unescape(file.name); if (file.is_directory || file.name == "." || file.name == "..") { continue; } num_of_items++; } // TODO: check for actual content inside the directory shared_ptr curr_find = make_shared(); curr_find->type = CELL_SEARCH_CONTENTTYPE_MUSICLIST; curr_find->repeat_mode = repeat_mode; curr_find->context_option = context_option; if (dir_path.length() > CELL_SEARCH_PATH_LEN_MAX) { // Create mapping which will be resolved to an actual hard link in VFS // by cellSearchPrepareFile std::string link = link_base + std::to_string(hash) + entry.name; strcpy_trunc(curr_find->infoPath.contentPath, link); std::lock_guard lock(search.links_mutex); search.content_links.emplace( std::move(link), search_info::link_data{.path = dir_path, .is_dir = true}); } else { strcpy_trunc(curr_find->infoPath.contentPath, dir_path); } CellSearchMusicListInfo& info = curr_find->data.music_list; info.listType = CELL_SEARCH_LISTSEARCHTYPE_MUSIC_ALBUM; info.numOfItems = num_of_items; info.duration = 0; strcpy_trunc(info.title, entry.name); strcpy_trunc(info.artistName, "ARTIST NAME"); content_map.map.emplace(dir_hash, curr_find); const u128 content_id_128 = dir_hash; std::memcpy(contents_id->data, &content_id_128, CELL_SEARCH_CONTENT_ID_SIZE); cellSearch.warning( "find_content_id: found music list %s (path control: '%s')", to_string(), dir_path); return CELL_OK; } continue; } // Search the subfolders. We assume all music is located in a depth of 2 // (max_depth, root + 1 folder + file). for (auto&& item : fs::dir(vfs_dir_path)) { if (item.is_directory || item.name == "." || item.name == "..") { continue; } const std::string file_path = dir_path + "/" + item.name; const u64 file_hash = std::hash()(file_path); if (hash == file_hash) { const auto [success, mi] = utils::get_media_info( vfs_dir_path + "/" + item.name, 1); // AVMEDIA_TYPE_AUDIO if (!success) { continue; } shared_ptr curr_find = make_shared(); curr_find->type = CELL_SEARCH_CONTENTTYPE_MUSIC; curr_find->repeat_mode = repeat_mode; curr_find->context_option = context_option; if (file_path.length() > CELL_SEARCH_PATH_LEN_MAX) { // Create mapping which will be resolved to an actual hard link in VFS // by cellSearchPrepareFile const size_t ext_offset = item.name.find_last_of('.'); std::string link = link_base + std::to_string(hash) + item.name.substr(ext_offset); strcpy_trunc(curr_find->infoPath.contentPath, link); std::lock_guard lock(search.links_mutex); search.content_links.emplace( std::move(link), search_info::link_data{.path = file_path, .is_dir = false}); } else { strcpy_trunc(curr_find->infoPath.contentPath, file_path); } populate_music_info(curr_find->data.music, mi, item); content_map.map.emplace(file_hash, curr_find); const u128 content_id_128 = file_hash; std::memcpy(contents_id->data, &content_id_128, CELL_SEARCH_CONTENT_ID_SIZE); cellSearch.warning( "find_content_id: found music track %s (path control: '%s')", to_string(), file_path); return CELL_OK; } } } // content ID not found return CELL_SEARCH_ERROR_CONTENT_NOT_FOUND; }