diff options
author | Jean-Francois Mauguit <jfmauguit@mac.com> | 2024-09-24 09:03:25 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-24 09:03:25 -0400 |
commit | bab614c421ed7ae329d26bf028c4a3b1d2450f5a (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/playlist/Playlists.cpp | |
parent | 4bde6044fddf053f31795b9eaccdd2a5a527d21f (diff) | |
parent | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (diff) | |
download | winamp-bab614c421ed7ae329d26bf028c4a3b1d2450f5a.tar.gz |
Merge pull request #5 from WinampDesktop/community
Merge to main
Diffstat (limited to 'Src/playlist/Playlists.cpp')
-rw-r--r-- | Src/playlist/Playlists.cpp | 746 |
1 files changed, 746 insertions, 0 deletions
diff --git a/Src/playlist/Playlists.cpp b/Src/playlist/Playlists.cpp new file mode 100644 index 00000000..71fd1a0e --- /dev/null +++ b/Src/playlist/Playlists.cpp @@ -0,0 +1,746 @@ +#include <algorithm> +#include "playlists.h" +#include "api__playlist.h" +#include "PlaylistsXML.h" +#include <shlwapi.h> +#include <limits.h> +#include <strsafe.h> +#pragma comment(lib, "Rpcrt4") +using namespace Nullsoft::Utility; + +/* +benski> Notes to maintainers +be sure to call DelayLoad() before doing anything. +This is mainly done because the XML parsing service isn't guaranteed to be registered before this service. +It also improves load time. +*/ + +/* --------------------------------------------- */ + +PlaylistInfo::PlaylistInfo() +{ + filename[0] = 0; + title[0] = 0; + length = 0; + numItems = 0; + iTunesID = 0; + cloud = 0; + + UuidCreate(&guid); +} + +PlaylistInfo::PlaylistInfo( const wchar_t *_filename, const wchar_t *_title, GUID playlist_guid ) +{ + StringCbCopyW( filename, sizeof( filename ), _filename ); + if ( _title ) + StringCbCopyW( title, sizeof( title ), _title ); + else + title[ 0 ] = 0; + + length = 0; + numItems = 0; + + if ( playlist_guid == INVALID_GUID ) + UuidCreate( &guid ); + else + guid = playlist_guid; + + iTunesID = 0; + cloud = 0; +} + +PlaylistInfo::PlaylistInfo( const PlaylistInfo © ) +{ + StringCbCopyW( filename, sizeof( filename ), copy.filename ); + StringCbCopyW( title, sizeof( title ), copy.title ); + + length = copy.length; + numItems = copy.numItems; + guid = copy.guid; + iTunesID = copy.iTunesID; + cloud = copy.cloud; +} + +/* --------------------------------------------- */ +Playlists::Playlists() +{ + iterator = 0; + triedLoaded = false; + loaded = false; + dirty = false; +} + +bool Playlists::DelayLoad() +{ + if ( triedLoaded ) + return loaded; + + PlaylistsXML loader( this ); + + const wchar_t *g_path = WASABI_API_APP->path_getUserSettingsPath(); + wchar_t playlistsFilename[ MAX_PATH ] = { 0 }; + wchar_t oldPlaylistsFilename[ MAX_PATH ] = { 0 }; + wchar_t newPlaylistsFolder[ MAX_PATH ] = { 0 }; + + PathCombineW( playlistsFilename, g_path, L"plugins" ); + PathAppendW( playlistsFilename, L"ml" ); + PathAppendW( playlistsFilename, L"playlists" ); + CreateDirectoryW( playlistsFilename, NULL ); + lstrcpynW( newPlaylistsFolder, playlistsFilename, MAX_PATH ); + PathAppendW( playlistsFilename, L"playlists.xml" ); + + PathCombineW( oldPlaylistsFilename, g_path, L"plugins" ); + PathAppendW( oldPlaylistsFilename, L"ml" ); + PathAppendW( oldPlaylistsFilename, L"playlists.xml" ); + + bool migrated = false; + if ( PathFileExistsW( oldPlaylistsFilename ) && !PathFileExistsW( playlistsFilename ) ) + { + if ( MoveFileW( oldPlaylistsFilename, playlistsFilename ) ) + { + migrated = true; + PathRemoveFileSpecW( oldPlaylistsFilename ); + } + } + + switch ( loader.LoadFile( playlistsFilename ) ) + { + case PLAYLISTSXML_SUCCESS: + loaded = true; + triedLoaded = true; + if ( AGAVE_API_STATS ) + AGAVE_API_STATS->SetStat( api_stats::PLAYLIST_COUNT, (int)playlists.size() ); + + if ( playlists.size() && migrated ) + { + for ( PlaylistInfo l_playlist : playlists ) + { + wchar_t path[ MAX_PATH ] = { 0 }, file[ MAX_PATH ] = { 0 }; + lstrcpynW( file, l_playlist.filename, MAX_PATH ); + PathStripPathW( file ); + PathCombineW( path, oldPlaylistsFilename, file ); + if ( PathFileExistsW( path ) ) + { + wchar_t new_path[ MAX_PATH ] = { 0 }; + PathCombineW( new_path, newPlaylistsFolder, file ); + MoveFileW( path, new_path ); + } + } + dirty = true; + Flush(); + } + + break; + case PLAYLISTSXML_NO_PARSER: + // if there's XML parser, we'll try again on the off-chance it eventually gets loaded (we might still be in the midst of loading the w5s/wac components) + break; + default: + loaded = true; + triedLoaded = true; + break; + } + + return loaded; +} + +void Playlists::Lock() +{ + playlistsGuard.Lock(); +} + +void Playlists::Unlock() +{ + playlistsGuard.Unlock(); +} + +size_t Playlists::GetIterator() +{ + return iterator; +} + +static void WriteEscaped( FILE *fp, const wchar_t *str ) +{ + // TODO: for speed optimization, + // we should wait until we hit a special character + // and write out everything else so before it, + // like how ASX loader does it + while ( str && *str ) + { + switch ( *str ) + { + case L'&': + fputws( L"&", fp ); + break; + case L'>': + fputws( L">", fp ); + break; + case L'<': + fputws( L"<", fp ); + break; + case L'\'': + fputws( L"'", fp ); + break; + case L'\"': + fputws( L""", fp ); + break; + default: + fputwc( *str, fp ); + break; + } + + // write out the whole UTF-16 character + wchar_t *next = CharNextW( str ); + while ( ++str != next ) + fputwc( *str, fp ); + } +} + +bool TitleSortAsc( PlaylistInfo &item1, PlaylistInfo &item2 ) +{ + int comp = CompareStringW( LOCALE_USER_DEFAULT, NORM_IGNORECASE | NORM_IGNOREWIDTH, item1.title, -1, item2.title, -1 ); + + return comp == CSTR_LESS_THAN; +} + +bool TitleSortDesc( PlaylistInfo &item1, PlaylistInfo &item2 ) +{ + int comp = CompareStringW( LOCALE_USER_DEFAULT, NORM_IGNORECASE | NORM_IGNOREWIDTH, item1.title, -1, item2.title, -1 ); + + return comp == CSTR_GREATER_THAN; +} + +bool NumberOfEntrySortAsc( PlaylistInfo &item1, PlaylistInfo &item2 ) +{ + return !!( item1.numItems < item2.numItems ); +} + +bool NumberOfEntrySortDesc( PlaylistInfo &item1, PlaylistInfo &item2 ) +{ + return !!( item1.numItems > item2.numItems ); +} + +int Playlists::Sort( size_t sort_type ) +{ + if ( !DelayLoad() ) + return 0; + + int sorted = 1; + + switch ( sort_type ) + { + case SORT_TITLE_ASCENDING: + std::sort( playlists.begin(), playlists.end(), TitleSortAsc ); + break; + case SORT_TITLE_DESCENDING: + std::sort( playlists.begin(), playlists.end(), TitleSortDesc ); + break; + case SORT_NUMBER_ASCENDING: + std::sort( playlists.begin(), playlists.end(), NumberOfEntrySortAsc ); + break; + case SORT_NUMBER_DESCENDING: + std::sort( playlists.begin(), playlists.end(), NumberOfEntrySortDesc ); + break; + default: + sorted = 0; + break; + } + + dirty = true; + + if ( sorted ) + Flush(); + + return sorted; +} + +void Playlists::Flush() +{ + AutoLockT<Playlists> lock( this ); + + if ( !triedLoaded && !loaded ) // if the playlists.xml file was never even attempted to be loaded, don't overwrite + return; + + if ( !dirty ) // if we've not seen any changes then no need to re-save + return; + + const wchar_t *g_path = WASABI_API_APP->path_getUserSettingsPath(); + + wchar_t rootPath[ MAX_PATH ] = { 0 }; + wchar_t playlistsBackupFilename[ MAX_PATH ] = { 0 }; + wchar_t playlistsDestination[ MAX_PATH ] = { 0 }; + + PathCombineW( rootPath, g_path, L"plugins" ); + CreateDirectoryW( rootPath, NULL ); + PathAppendW( rootPath, L"ml" ); + CreateDirectoryW( rootPath, NULL ); + PathAppendW( rootPath, L"playlists" ); + CreateDirectoryW( rootPath, NULL ); + + int g_path_size = wcslen( rootPath ); + PathCombineW( playlistsBackupFilename, rootPath, L"playlists.xml.backup" ); + PathCombineW( playlistsDestination, rootPath, L"playlists.xml" ); + + CopyFileW( playlistsDestination, playlistsBackupFilename, FALSE ); + + FILE *fp = _wfopen( playlistsDestination, L"wb" ); + if ( !fp ) // bah + { + dirty = false; + return; + } + + fseek( fp, 0, SEEK_SET ); + fputwc( L'\xFEFF', fp ); + fwprintf( fp, L"<?xml version=\"1.0\" encoding=\"UTF-16\"?>" ); + fwprintf( fp, L"<playlists playlists=\"%u\">", (unsigned int)playlists.size() ); + + if ( AGAVE_API_STATS ) + AGAVE_API_STATS->SetStat( api_stats::PLAYLIST_COUNT, (int)playlists.size() ); + + for ( PlaylistInfo &l_play_list_info : playlists ) + { + fputws( L"<playlist filename=\"", fp ); + const wchar_t *fn = l_play_list_info.filename; + + if ( !_wcsnicmp( rootPath, fn, g_path_size ) ) + { + fn += g_path_size; + + if ( *fn == L'\\' ) + ++fn; + } + + WriteEscaped( fp, fn ); + + fputws( L"\" title=\"", fp ); + WriteEscaped( fp, l_play_list_info.title ); + + GUID guid = l_play_list_info.guid; + + fwprintf( fp, L"\" id=\"{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\"", + (int)guid.Data1, (int)guid.Data2, (int)guid.Data3, + (int)guid.Data4[ 0 ], (int)guid.Data4[ 1 ], + (int)guid.Data4[ 2 ], (int)guid.Data4[ 3 ], + (int)guid.Data4[ 4 ], (int)guid.Data4[ 5 ], + (int)guid.Data4[ 6 ], (int)guid.Data4[ 7 ] ); + + fwprintf( fp, L" songs=\"%u\" seconds=\"%u\"", l_play_list_info.numItems, l_play_list_info.length ); + + if ( l_play_list_info.iTunesID ) + fwprintf( fp, L" iTunesID=\"%I64u\"", l_play_list_info.iTunesID ); + + if ( l_play_list_info.cloud ) + fwprintf( fp, L" cloud=\"1\"" ); + + fwprintf( fp, L"/>" ); + } + + fwprintf( fp, L"</playlists>" ); + fclose( fp ); + dirty = false; +} + +size_t Playlists::GetCount() +{ + DelayLoad(); + + return playlists.size(); +} + +const wchar_t *Playlists::GetFilename( size_t index ) +{ + if ( !DelayLoad() || index >= playlists.size() ) + return 0; + + return playlists[ index ].filename; +} + +const wchar_t *Playlists::GetName( size_t index ) +{ + if ( !DelayLoad() || index >= playlists.size() ) + return 0; + + return playlists[ index ].title; +} + +GUID Playlists::GetGUID( size_t index ) +{ + if ( !DelayLoad() || index >= playlists.size() ) + return INVALID_GUID; + + return playlists[ index ].guid; +} + +int Playlists::GetPosition( GUID playlist_guid, size_t *index ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + size_t indexCount = 0; + for ( PlaylistInfo &l_play_list_info : playlists ) + { + if ( l_play_list_info.guid == playlist_guid ) + { + *index = indexCount; + return API_PLAYLISTS_SUCCESS; + } + + ++indexCount; + } + + return API_PLAYLISTS_FAILURE; +} + +template <class val_t> +static int GetWithSize( void *data, size_t dataLen, val_t value ) +{ + switch ( dataLen ) + { + case 1: + { + if ( value > _UI8_MAX ) // check for overflow + return API_PLAYLISTS_BAD_SIZE; + + *(uint8_t *)data = (uint8_t)value; + + return API_PLAYLISTS_SUCCESS; + } + case 2: + { + if ( value > _UI16_MAX ) // check for overflow + return API_PLAYLISTS_BAD_SIZE; + + *(uint16_t *)data = (uint16_t)value; + + return API_PLAYLISTS_SUCCESS; + } + case 4: + { + if ( value > _UI32_MAX ) + return API_PLAYLISTS_BAD_SIZE; + + *(uint32_t *)data = (uint32_t)value; + + return API_PLAYLISTS_SUCCESS; + } + case 8: + { + if ( value > _UI64_MAX ) + return API_PLAYLISTS_BAD_SIZE; + + *(uint64_t *)data = (uint64_t)value; + + return API_PLAYLISTS_SUCCESS; + } + } + + return API_PLAYLISTS_BAD_SIZE; +} + +template <class val_t> +static int SetWithSize( void *data, size_t dataLen, val_t *value ) +{ + switch ( dataLen ) + { + case 1: + { + *value = ( val_t ) * (uint8_t *)data; + return API_PLAYLISTS_SUCCESS; + } + case 2: + { + *value = ( val_t ) * (uint16_t *)data; + return API_PLAYLISTS_SUCCESS; + } + case 4: + { + *value = ( val_t ) * (uint32_t *)data; + return API_PLAYLISTS_SUCCESS; + } + case 8: + { + *value = ( val_t ) * (uint64_t *)data; + return API_PLAYLISTS_SUCCESS; + } + } + + return API_PLAYLISTS_BAD_SIZE; +} + +int Playlists::GetInfo( size_t index, GUID info, void *data, size_t dataLen ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + if ( info == api_playlists_itemCount ) + return GetWithSize( data, dataLen, playlists[ index ].numItems ); + else if ( info == api_playlists_totalTime ) + return GetWithSize( data, dataLen, playlists[ index ].length ); + else if ( info == api_playlists_iTunesID ) + return GetWithSize( data, dataLen, playlists[ index ].iTunesID ); + else if ( info == api_playlists_cloud ) + return GetWithSize( data, dataLen, playlists[ index ].cloud ); + + return API_PLAYLISTS_UNKNOWN_INFO_GUID; +} + +int Playlists::MoveBefore( size_t index1, size_t index2 ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index1 >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + PlaylistInfo copy = playlists[ index1 ]; + if ( index2 >= playlists.size() ) + { + playlists.push_back( copy ); + playlists.erase(playlists.begin() + index1 ); + } + else + { + playlists.insert(playlists.begin() + index2, copy ); + if ( index1 >= index2 ) + index1++; + playlists.erase(playlists.begin() + index1 ); + } + + dirty = true; + ++iterator; + + return API_PLAYLISTS_SUCCESS; +} + +size_t Playlists::AddPlaylist( const wchar_t *filename, const wchar_t *playlistName, GUID playlist_guid ) +{ + if ( !DelayLoad() ) + return -1; + + AutoLockT<Playlists> lock( this ); + + if ( playlist_guid != INVALID_GUID ) + { + for ( size_t index = 0; index < playlists.size(); index++ ) + { + if ( playlists[ index ].guid == playlist_guid ) + { + if ( lstrcmpiW( playlists[ index ].title, playlistName ) ) + { + dirty = true; + StringCbCopyW( playlists[ index ].title, sizeof( playlists[ index ].title ), playlistName ); + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_RENAMED, index, 0 ); + } + + return -2; + } + } + } + + size_t newIndex = AddPlaylist_NoCallback( filename, playlistName, playlist_guid ); + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_ADDED, newIndex, 0 ); + + return newIndex; +} + +size_t Playlists::AddCloudPlaylist( const wchar_t *filename, const wchar_t *playlistName, GUID playlist_guid ) +{ + if ( !DelayLoad() ) + return -1; + + AutoLockT<Playlists> lock( this ); + + if ( playlist_guid != INVALID_GUID ) + { + for ( size_t index = 0; index < playlists.size(); index++ ) + { + if ( playlists[ index ].guid == playlist_guid ) + { + // we make sure that this playlist has a 'cloud' flag + // as without it, our detection of thing isn't ideal. + if ( !playlists[ index ].cloud ) + playlists[ index ].cloud = 1; + + if ( lstrcmpW( playlists[ index ].title, playlistName ) ) + { + dirty = true; + StringCbCopyW( playlists[ index ].title, sizeof( playlists[ index ].title ), playlistName ); + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_RENAMED, index, 0 ); + } + + return -2; + } + } + } + + size_t newIndex = AddPlaylist_NoCallback( filename, playlistName, playlist_guid ); + int cloud = 1; + SetInfo( newIndex, api_playlists_cloud, &cloud, sizeof( cloud ) ); + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_ADDED, newIndex, 0 ); + + return newIndex; +} + +size_t Playlists::AddPlaylist_internal( const wchar_t *filename, const wchar_t *playlistName, GUID playlist_guid, size_t numItems, size_t length, uint64_t iTunesID, size_t cloud ) +{ + PlaylistInfo newPlaylist( filename, playlistName, playlist_guid ); + newPlaylist.numItems = (int)numItems; + newPlaylist.length = (int)length; + newPlaylist.iTunesID = iTunesID; + newPlaylist.cloud = (int)cloud; + + playlists.push_back( newPlaylist ); + size_t newIndex = playlists.size() - 1; + + return newIndex; +} + +size_t Playlists::AddPlaylist_NoCallback( const wchar_t *filename, const wchar_t *playlistName, GUID playlist_guid ) +{ + AutoLockT<Playlists> lock( this ); + PlaylistInfo newPlaylist( filename, playlistName, playlist_guid ); + + dirty = true; + playlists.push_back( newPlaylist ); + + ++iterator; + + size_t newIndex = playlists.size() - 1; + + return newIndex; +} + +int Playlists::SetGUID( size_t index, GUID playlist_guid ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + dirty = true; + playlists[ index ].guid = playlist_guid; + + return API_PLAYLISTS_SUCCESS; +} + +int Playlists::RenamePlaylist( size_t index, const wchar_t *name ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + dirty = true; + + if ( lstrcmpW( playlists[ index ].title, name ) ) + { + StringCbCopyW( playlists[ index ].title, sizeof( playlists[ index ].title ), name ); + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_RENAMED, index, 0 ); + } + + return API_PLAYLISTS_SUCCESS; +} + +int Playlists::MovePlaylist( size_t index, const wchar_t *filename ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + dirty = true; + StringCbCopyW( playlists[ index ].filename, sizeof( playlists[ index ].filename ), filename ); + iterator++; + + return API_PLAYLISTS_SUCCESS; +} + +int Playlists::SetInfo( size_t index, GUID info, void *data, size_t dataLen ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + dirty = true; + if ( info == api_playlists_itemCount ) + return SetWithSize( data, dataLen, &playlists[ index ].numItems ); + else if ( info == api_playlists_totalTime ) + return SetWithSize( data, dataLen, &playlists[ index ].length ); + else if ( info == api_playlists_iTunesID ) + return SetWithSize( data, dataLen, &playlists[ index ].iTunesID ); + else if ( info == api_playlists_cloud ) + return SetWithSize( data, dataLen, &playlists[ index ].cloud ); + + dirty = false; + + return API_PLAYLISTS_UNKNOWN_INFO_GUID; +} + +int Playlists::RemovePlaylist( size_t index ) +{ + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + if ( index >= playlists.size() ) + return API_PLAYLISTS_INVALID_INDEX; + + dirty = true; + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_REMOVED_PRE, index, 0 ); + playlists.erase(playlists.begin() + index ); + iterator++; + WASABI_API_SYSCB->syscb_issueCallback( api_playlists::SYSCALLBACK, api_playlists::PLAYLIST_REMOVED_POST, index, 0 ); + + return API_PLAYLISTS_SUCCESS; +} + +int Playlists::ClearPlaylists() +{ + AutoLockT<Playlists> lock( this ); + + if ( !DelayLoad() ) + return API_PLAYLISTS_UNABLE_TO_LOAD_PLAYLISTS; + + dirty = true; + playlists.clear(); + + return API_PLAYLISTS_SUCCESS; +} + +const PlaylistInfo &Playlists::GetPlaylistInfo(size_t i) +{ + return playlists[i]; +} + +#define CBCLASS Playlists +START_DISPATCH; +VCB( API_PLAYLISTS_LOCK, Lock ); +VCB( API_PLAYLISTS_UNLOCK, Unlock ); +CB( API_PLAYLISTS_GETITERATOR, GetIterator ); +VCB( API_PLAYLISTS_FLUSH, Flush ); +CB( API_PLAYLISTS_GETCOUNT, GetCount ); +CB( API_PLAYLISTS_GETFILENAME, GetFilename ); +CB( API_PLAYLISTS_GETNAME, GetName ); +CB( API_PLAYLISTS_GETGUID, GetGUID ); +CB( API_PLAYLISTS_GETPOSITION, GetPosition ); +CB( API_PLAYLISTS_GETINFO, GetInfo ); +CB( API_PLAYLISTS_MOVEBEFORE, MoveBefore ); +CB( API_PLAYLISTS_ADDPLAYLIST, AddPlaylist ); +CB( API_PLAYLISTS_ADDPLAYLISTNOCB, AddPlaylist_NoCallback ); +CB( API_PLAYLISTS_ADDCLOUDPLAYLIST, AddCloudPlaylist ); +CB( API_PLAYLISTS_SETGUID, SetGUID ); +CB( API_PLAYLISTS_RENAMEPLAYLIST, RenamePlaylist ); +CB( API_PLAYLISTS_MOVEPLAYLIST, MovePlaylist ); +CB( API_PLAYLISTS_SETINFO, SetInfo ); +CB( API_PLAYLISTS_REMOVEPLAYLIST, RemovePlaylist ); +CB( API_PLAYLISTS_CLEARPLAYLISTS, ClearPlaylists ); +CB( API_PLAYLISTS_SORT, Sort ); +END_DISPATCH; +#undef CBCLASS
\ No newline at end of file |