Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 62 additions & 8 deletions src/StateFile.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@
#include "io/BufferedOutputStream.hxx"
#include "storage/StorageState.hxx"
#include "Partition.hxx"
#include "config/PartitionConfig.hxx"
#include "Instance.hxx"
#include "SongLoader.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
#include "util/StringCompare.hxx"

#include <exception>

#define PARTITION_STATE "partition: "

static constexpr Domain state_file_domain("state_file");

StateFile::StateFile(StateFileConfig &&_config,
Expand Down Expand Up @@ -55,14 +59,23 @@ StateFile::IsModified() const noexcept
inline void
StateFile::Write(BufferedOutputStream &os)
{
partition.mixer_memento.SaveSoftwareVolumeState(os);
audio_output_state_save(os, partition.outputs);
for (auto &current_partition : partition.instance.partitions) {
const bool is_default_partition =
&current_partition == &partition.instance.partitions.front();
if (!is_default_partition) // Write partition header except for default partition
os.Fmt(PARTITION_STATE "{}\n", current_partition.name);
current_partition.mixer_memento.SaveSoftwareVolumeState(os);
audio_output_state_save(os, current_partition.outputs);
playlist_state_save(os, current_partition.playlist, current_partition.pc);

#ifdef ENABLE_DATABASE
storage_state_save(os, partition.instance);
if (is_default_partition) {
// Only save storage state once and do it in the default partition
storage_state_save(os, partition.instance);
}
#endif
}

playlist_state_save(os, partition.playlist, partition.pc);
}

inline void
Expand Down Expand Up @@ -106,13 +119,18 @@ try {
const SongLoader song_loader(nullptr, nullptr);
#endif

Partition *current_partition = &partition;

const char *line;
while ((line = file.ReadLine()) != nullptr) {
success = partition.mixer_memento.LoadSoftwareVolumeState(line, partition.outputs) ||
audio_output_state_read(line, partition.outputs) ||
success = current_partition->mixer_memento.LoadSoftwareVolumeState(line,
partition.outputs) ||
audio_output_state_read(line, partition.outputs, current_partition) ||
playlist_state_restore(config, line, file, song_loader,
partition.playlist,
partition.pc);
current_partition->playlist,
current_partition->pc) ||
PartitionSwitch(line, current_partition);

#ifdef ENABLE_DATABASE
success = success || storage_state_restore(line, file, partition.instance);
#endif
Expand Down Expand Up @@ -140,3 +158,39 @@ StateFile::OnTimeout() noexcept
{
Write();
}

/**
* Attempts to switch the current partition based on a state file line.
*
* @param line The line from the state file to parse
* @param current_partition Reference to pointer that will be updated to point
* to the target partition
* @return true if the line was a partition switch command, false otherwise
*/
bool StateFile::PartitionSwitch(const char *line,
Partition *&current_partition) noexcept {
// Check if this line contains a partition switch command
line = StringAfterPrefix(line, PARTITION_STATE);
if (line == nullptr)
return false;

// Try to find existing partition
Partition *new_partition = partition.instance.FindPartition(line);
if (new_partition != nullptr) {
current_partition = new_partition;
FmtDebug(state_file_domain, "Switched to existing partition '{}'",
current_partition->name);
return true;
}

// Partition doesn't exist, create it
partition.instance.partitions.emplace_back(partition.instance, line,
PartitionConfig{});
current_partition = &partition.instance.partitions.back();
current_partition->UpdateEffectiveReplayGainMode();

FmtDebug(state_file_domain, "Created partition '{}' and switched to it",
current_partition->name);

return true;
}
6 changes: 6 additions & 0 deletions src/StateFile.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,10 @@ private:

/* callback for #timer_event */
void OnTimeout() noexcept;

/**
* Handle partition lines in the state file
*/
bool PartitionSwitch(const char *line,
Partition *&current_partition) noexcept;
};
54 changes: 48 additions & 6 deletions src/output/State.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,35 @@
#include "Log.hxx"
#include "io/BufferedOutputStream.hxx"
#include "util/StringCompare.hxx"
#include "Partition.hxx"
#include "config/PartitionConfig.hxx"

#include <fmt/format.h>

#include <stdlib.h>

#define AUDIO_DEVICE_STATE "audio_device_state:"
#define DEFAULT_PARTITION "default"

unsigned audio_output_state_version;

/**
* Iterate the instance and save audio output state configuration.
*
* Writes the name of the audio outputs, the partitions they are assigned to,
* and the state of the output.
*
* Writes a configuration line this format:
* AUDIO_DEVICE_STATE<value>:<device>:<partition>
*
* Where:
* <value> = 0 (disabled) or 1 (enabled)
* <device> = output device name
*
* @param os The output stream
* @param outputs The outputs assigned to this partition
* @return nothing
*/
void
audio_output_state_save(BufferedOutputStream &os,
const MultipleOutputs &outputs)
Expand All @@ -34,8 +54,25 @@ audio_output_state_save(BufferedOutputStream &os,
}
}

/**
* Parse and apply audio output state configuration.
*
* Reads a configuration line in one of these formats:
* AUDIO_DEVICE_STATE<value>:<device>
* AUDIO_DEVICE_STATE<value>:<device>:<partition>
*
* Where:
* <value> = 0 (disabled) or 1 (enabled)
* <device> = output device name
* <partition> = optional partition name
*
* @param line The configuration line to parse
* @param outputs The collection of audio outputs to modify
* @param current_partition The partition to which the outputs belong
* @return true if the line was valid and processed, false on parse error
*/
bool
audio_output_state_read(const char *line, MultipleOutputs &outputs)
audio_output_state_read(const char *line, MultipleOutputs &outputs, Partition *current_partition)
{
long value;
char *endptr;
Expand All @@ -49,10 +86,6 @@ audio_output_state_read(const char *line, MultipleOutputs &outputs)
if (*endptr != ':' || (value != 0 && value != 1))
return false;

if (value != 0)
/* state is "enabled": no-op */
return true;

name = endptr + 1;
auto *ao = outputs.FindByName(name);
if (ao == nullptr) {
Expand All @@ -61,7 +94,16 @@ audio_output_state_read(const char *line, MultipleOutputs &outputs)
return true;
}

ao->LockSetEnabled(false);
if (current_partition->name != DEFAULT_PARTITION) {
// Move the output to this partition
FmtDebug(output_domain,
"Moving device {:?} from default to partition {:?}",
name, current_partition->name);
current_partition->outputs.AddMoveFrom(std::move(*ao), value != 0);
return true;
}

ao->LockSetEnabled(value != 0);
return true;
}

Expand Down
3 changes: 2 additions & 1 deletion src/output/State.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@

class MultipleOutputs;
class BufferedOutputStream;
struct Partition;

bool
audio_output_state_read(const char *line, MultipleOutputs &outputs);
audio_output_state_read(const char *line, MultipleOutputs &outputs, Partition *partition);

void
audio_output_state_save(BufferedOutputStream &os,
Expand Down
1 change: 1 addition & 0 deletions src/queue/PlaylistState.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ playlist_state_restore(const StateFileConfig &config,
} else if (StringStartsWith(line,
PLAYLIST_STATE_FILE_PLAYLIST_BEGIN)) {
playlist_state_load(file, song_loader, playlist);
break;
}
}

Expand Down
40 changes: 40 additions & 0 deletions test/MockPlaylistSong.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project

/**
* Mock implementation of playlist_check_translate_song for testing.
*
* This minimal mock allows songs to be loaded into the queue during tests
* without requiring a real database or file system. It simply returns true
* for all songs, bypassing the normal validation that would fail in a test
* environment.
*/

#include "playlist/PlaylistSong.hxx"
#include "song/DetachedSong.hxx"

/**
* Mock implementation that always allows songs to load.
*
* In production, this function validates that:
* - Songs exist in the database or filesystem
* - URIs are properly formatted
* - Files are accessible
*
* For testing, we bypass all validation and allow any song to load.
* This enables testing of state file read/write logic without needing
* a real music database.
*
* @param song The song to validate (modified in place if needed)
* @param base_uri Base URI for resolving relative paths (unused in mock)
* @param loader Song loader for database access (unused in mock)
* @return Always returns true to allow the song to be added to the queue
*/
bool
playlist_check_translate_song(DetachedSong &song [[maybe_unused]],
[[maybe_unused]] std::string_view base_uri,
[[maybe_unused]] const SongLoader &loader) noexcept
{
// Always return true - allow all songs to load in tests
return true;
}
31 changes: 31 additions & 0 deletions test/MockStorage.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project

/**
* Mock storage implementation for testing.
*/

#include "MockStorage.hxx"
#include "util/StringCompare.hxx"

static std::unique_ptr<Storage>
CreateMockStorageURI([[maybe_unused]] EventLoop &event_loop, const char *uri)
{
// Accept any URI starting with "mock://"
if (!StringStartsWith(uri, "mock://"))
return nullptr;

return std::make_unique<MockStorage>(uri);
}

// Make prefixes array external linkage so it's accessible
const char *const mock_storage_prefixes[] = {
"mock://",
nullptr
};

const StoragePlugin mock_storage_plugin = {
.name = "mock",
.prefixes = mock_storage_prefixes,
.create_uri = CreateMockStorageURI,
};
65 changes: 65 additions & 0 deletions test/MockStorage.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright The Music Player Daemon Project

/**
* Mock storage implementation for testing.
*
* This provides a minimal in-memory storage that can be mounted and
* unmounted during tests without requiring actual filesystem or network
* resources.
*/

#ifndef MPD_MOCK_STORAGE_HXX
#define MPD_MOCK_STORAGE_HXX

#include "storage/StorageInterface.hxx"
#include "storage/StoragePlugin.hxx"
#include "storage/FileInfo.hxx"
#include "fs/AllocatedPath.hxx"
#include "input/InputStream.hxx"

/**
* A minimal mock storage implementation that stores only its URI.
*
* This is sufficient for testing state file read/write operations,
* which only need to serialize and deserialize mount points.
*/
class MockStorage final : public Storage {
std::string uri;

public:
explicit MockStorage(std::string_view _uri) noexcept
: uri(_uri) {}

/* virtual methods from class Storage */
StorageFileInfo GetInfo(std::string_view, bool) override {
throw std::runtime_error("Not implemented in mock");
}

std::unique_ptr<StorageDirectoryReader> OpenDirectory(std::string_view) override {
throw std::runtime_error("Not implemented in mock");
}

std::string MapUTF8(std::string_view) const noexcept override {
return uri;
}

AllocatedPath MapFS(std::string_view) const noexcept override {
return nullptr;
}

std::string_view MapToRelativeUTF8(std::string_view) const noexcept override {
return {};
}

InputStreamPtr OpenFile(std::string_view, Mutex &) override {
return nullptr;
}
};

/**
* Storage plugin for creating mock storage instances.
*/
extern const StoragePlugin mock_storage_plugin;

#endif
Loading