diff --git a/NEWS b/NEWS index 6ae634e73..0caf58e5d 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,8 @@ ver 0.22 (not yet released) - ffmpeg: allow partial reads * archive - iso9660: support seeking +* playlist + - cue: integrate contents in database * decoder - mad: remove option "gapless", always do gapless - sidplay: add option "default_genre" diff --git a/src/db/plugins/simple/Directory.hxx b/src/db/plugins/simple/Directory.hxx index 32c20c061..1647dbfad 100644 --- a/src/db/plugins/simple/Directory.hxx +++ b/src/db/plugins/simple/Directory.hxx @@ -44,6 +44,12 @@ static constexpr unsigned DEVICE_INARCHIVE = -1; */ static constexpr unsigned DEVICE_CONTAINER = -2; +/** + * Virtual directory that is really a playlist file (special value for + * Directory::device). + */ +static constexpr unsigned DEVICE_PLAYLIST = -3; + class SongFilter; struct Directory { @@ -117,6 +123,7 @@ public: */ bool IsReallyAFile() const noexcept { return device == DEVICE_INARCHIVE || + device == DEVICE_PLAYLIST || device == DEVICE_CONTAINER; } diff --git a/src/db/plugins/simple/DirectorySave.cxx b/src/db/plugins/simple/DirectorySave.cxx index 1dedba063..ade511e97 100644 --- a/src/db/plugins/simple/DirectorySave.cxx +++ b/src/db/plugins/simple/DirectorySave.cxx @@ -50,6 +50,9 @@ DeviceToTypeString(unsigned device) noexcept case DEVICE_CONTAINER: return "container"; + case DEVICE_PLAYLIST: + return "playlist"; + default: return nullptr; } @@ -63,6 +66,8 @@ ParseTypeString(const char *type) noexcept return DEVICE_INARCHIVE; else if (StringIsEqual(type, "container")) return DEVICE_CONTAINER; + else if (StringIsEqual(type, "playlist")) + return DEVICE_PLAYLIST; else return 0; } diff --git a/src/db/update/Playlist.cxx b/src/db/update/Playlist.cxx index 93275dc91..4d4a9bb1d 100644 --- a/src/db/update/Playlist.cxx +++ b/src/db/update/Playlist.cxx @@ -18,24 +18,91 @@ */ #include "Walk.hxx" +#include "UpdateDomain.hxx" #include "db/DatabaseLock.hxx" #include "db/PlaylistVector.hxx" #include "db/plugins/simple/Directory.hxx" +#include "song/DetachedSong.hxx" +#include "input/InputStream.hxx" +#include "playlist/PlaylistPlugin.hxx" #include "playlist/PlaylistRegistry.hxx" +#include "playlist/PlaylistStream.hxx" +#include "playlist/SongEnumerator.hxx" #include "storage/FileInfo.hxx" +#include "storage/StorageInterface.hxx" +#include "util/StringFormat.hxx" +#include "Log.hxx" + +void +UpdateWalk::UpdatePlaylistFile(Directory &parent, const char *name, + const StorageFileInfo &info, + const PlaylistPlugin &plugin) noexcept +{ + assert(plugin.open_stream); + + Directory *directory = + LockMakeVirtualDirectoryIfModified(parent, name, info, + DEVICE_PLAYLIST); + if (directory == nullptr) + /* not modified */ + return; + + const auto uri_utf8 = storage.MapUTF8(directory->GetPath()); + + FormatDebug(update_domain, "scanning playlist '%s'", uri_utf8.c_str()); + + try { + Mutex mutex; + auto e = plugin.open_stream(InputStream::OpenReady(uri_utf8.c_str(), + mutex)); + if (!e) { + /* unsupported URI? roll back.. */ + editor.LockDeleteDirectory(directory); + return; + } + + unsigned track = 0; + + while (true) { + auto song = e->NextSong(); + if (!song) + break; + + auto db_song = std::make_unique(std::move(*song), + *directory); + db_song->target = "../" + db_song->filename; + db_song->filename = StringFormat<64>("track%04u", + ++track); + + { + const ScopeDatabaseLock protect; + directory->AddSong(std::move(db_song)); + } + } + } catch (...) { + FormatError(std::current_exception(), + "Failed to scan playlist '%s'", uri_utf8.c_str()); + editor.LockDeleteDirectory(directory); + } +} bool UpdateWalk::UpdatePlaylistFile(Directory &directory, const char *name, const char *suffix, const StorageFileInfo &info) noexcept { - if (!playlist_suffix_supported(suffix)) + const auto *const plugin = FindPlaylistPluginBySuffix(suffix); + if (plugin == nullptr) return false; + if (plugin->as_folder) + UpdatePlaylistFile(directory, name, info, *plugin); + PlaylistInfo pi(name, info.mtime); const ScopeDatabaseLock protect; if (directory.playlists.UpdateOrInsert(std::move(pi))) modified = true; + return true; } diff --git a/src/db/update/Walk.hxx b/src/db/update/Walk.hxx index e54e398b9..72b0e7822 100644 --- a/src/db/update/Walk.hxx +++ b/src/db/update/Walk.hxx @@ -30,6 +30,7 @@ struct StorageFileInfo; struct Directory; struct ArchivePlugin; +struct PlaylistPlugin; class ArchiveFile; class Storage; class ExcludeList; @@ -118,6 +119,10 @@ private: } #endif + void UpdatePlaylistFile(Directory &parent, const char *name, + const StorageFileInfo &info, + const PlaylistPlugin &plugin) noexcept; + bool UpdatePlaylistFile(Directory &directory, const char *name, const char *suffix, const StorageFileInfo &info) noexcept; diff --git a/src/playlist/PlaylistPlugin.hxx b/src/playlist/PlaylistPlugin.hxx index 4661ed967..449435ccb 100644 --- a/src/playlist/PlaylistPlugin.hxx +++ b/src/playlist/PlaylistPlugin.hxx @@ -69,6 +69,12 @@ struct PlaylistPlugin { const char *const*suffixes = nullptr; const char *const*mime_types = nullptr; + /** + * If true, then playlists of this type are shown in the + * database as folders. + */ + bool as_folder = false; + constexpr PlaylistPlugin(const char *_name, std::unique_ptr (*_open_uri)(const char *uri, Mutex &mutex)) noexcept @@ -104,6 +110,12 @@ struct PlaylistPlugin { return copy; } + constexpr auto WithAsFolder(bool value=true) noexcept { + auto copy = *this; + copy.as_folder = value; + return copy; + } + /** * Does the plugin announce the specified URI scheme? */ diff --git a/src/playlist/plugins/CuePlaylistPlugin.cxx b/src/playlist/plugins/CuePlaylistPlugin.cxx index 137eb4e90..b8d645c94 100644 --- a/src/playlist/plugins/CuePlaylistPlugin.cxx +++ b/src/playlist/plugins/CuePlaylistPlugin.cxx @@ -72,5 +72,6 @@ static const char *const cue_playlist_mime_types[] = { const PlaylistPlugin cue_playlist_plugin = PlaylistPlugin("cue", cue_playlist_open_stream) + .WithAsFolder() .WithSuffixes(cue_playlist_suffixes) .WithMimeTypes(cue_playlist_mime_types);