Added support for comment tag, fixes #510
This commit is contained in:
+23
-22
@@ -6,7 +6,7 @@ Given the API limitations of folder navigation commands, it is recommended to pl
|
|||||||
|
|
||||||
The Subsonic API is enabled by default.
|
The Subsonic API is enabled by default.
|
||||||
|
|
||||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure that read access to the web server logs (and to the proxy, if applicable) is well protected.
|
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure the web server logs (and proxy logs, if applicable) are properly secured.
|
||||||
|
|
||||||
# OpenSubsonic API
|
# OpenSubsonic API
|
||||||
OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/)
|
OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/)
|
||||||
@@ -14,32 +14,33 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
|
|||||||
## Extra fields
|
## Extra fields
|
||||||
The following extra fields are implemented:
|
The following extra fields are implemented:
|
||||||
* `Album` response:
|
* `Album` response:
|
||||||
* `mediaType`
|
|
||||||
* `played`
|
|
||||||
* `musicBrainzId`
|
|
||||||
* `genres`
|
|
||||||
* `artists`
|
* `artists`
|
||||||
* `displayArtist`
|
|
||||||
* `releaseTypes`
|
|
||||||
* `moods`
|
|
||||||
* `originalReleaseDate`
|
|
||||||
* `isCompilation`
|
|
||||||
* `discTitles`: discs with no subtitle are omitted
|
* `discTitles`: discs with no subtitle are omitted
|
||||||
* `Child` response:
|
|
||||||
* `bitDepth`
|
|
||||||
* `samplingRate`
|
|
||||||
* `channelCount`
|
|
||||||
* `mediaType`
|
|
||||||
* `played`
|
|
||||||
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
|
||||||
* `genres`
|
|
||||||
* `artists`
|
|
||||||
* `displayArtist`
|
* `displayArtist`
|
||||||
* `albumArtists`
|
* `genres`
|
||||||
* `displayAlbumArtist`
|
* `isCompilation`
|
||||||
* `contributors`
|
* `played`
|
||||||
|
* `mediaType`
|
||||||
* `moods`
|
* `moods`
|
||||||
|
* `musicBrainzId`
|
||||||
|
* `originalReleaseDate`
|
||||||
|
* `releaseTypes`
|
||||||
|
* `Child` response:
|
||||||
|
* `albumArtists`
|
||||||
|
* `artists`
|
||||||
|
* `bitDepth`
|
||||||
|
* `channelCount`
|
||||||
|
* `comment`
|
||||||
|
* `contributors`
|
||||||
|
* `displayAlbumArtist`
|
||||||
|
* `displayArtist`
|
||||||
|
* `genres`
|
||||||
|
* `mediaType`
|
||||||
|
* `moods`
|
||||||
|
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
||||||
|
* `played`
|
||||||
* `replayGain`
|
* `replayGain`
|
||||||
|
* `samplingRate`
|
||||||
* `Artist` response:
|
* `Artist` response:
|
||||||
* `mediaType`
|
* `mediaType`
|
||||||
* `musicBrainzId`
|
* `musicBrainzId`
|
||||||
|
|||||||
@@ -125,6 +125,10 @@
|
|||||||
${playcount}
|
${playcount}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${<if-has-comment>}
|
||||||
|
<hr/>
|
||||||
|
<pre>${comment}</pre>
|
||||||
|
${</if-has-comment>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 62 };
|
static constexpr Version LMS_DATABASE_VERSION{ 63 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -664,6 +664,15 @@ SELECT
|
|||||||
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV62(Session& session)
|
||||||
|
{
|
||||||
|
// Add a new column comment
|
||||||
|
session.getDboSession()->execute("ALTER TABLE track ADD comment TEXT NOT NULL DEFAULT ''");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
|
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -702,6 +711,7 @@ SELECT
|
|||||||
{ 59, migrateFromV59 },
|
{ 59, migrateFromV59 },
|
||||||
{ 60, migrateFromV60 },
|
{ 60, migrateFromV60 },
|
||||||
{ 61, migrateFromV61 },
|
{ 61, migrateFromV61 },
|
||||||
|
{ 62, migrateFromV62 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ namespace lms::db
|
|||||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
||||||
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
|
||||||
|
void setComment(std::string_view comment) { _comment = comment; }
|
||||||
void clearArtistLinks();
|
void clearArtistLinks();
|
||||||
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
|
||||||
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
|
||||||
@@ -264,6 +265,8 @@ namespace lms::db
|
|||||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||||
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
|
||||||
|
std::string_view getComment() const { return _comment; }
|
||||||
|
|
||||||
// no artistLinkTypes means get all
|
// no artistLinkTypes means get all
|
||||||
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||||
std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
|
||||||
@@ -307,6 +310,8 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||||
|
Wt::Dbo::field(a, _comment, "comment");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
|
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
|
||||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
@@ -350,6 +355,7 @@ namespace lms::db
|
|||||||
std::optional<float> _trackReplayGain;
|
std::optional<float> _trackReplayGain;
|
||||||
std::optional<float> _releaseReplayGain;
|
std::optional<float> _releaseReplayGain;
|
||||||
std::string _artistDisplayName;
|
std::string _artistDisplayName;
|
||||||
|
std::string _comment;
|
||||||
|
|
||||||
Wt::Dbo::ptr<Release> _release;
|
Wt::Dbo::ptr<Release> _release;
|
||||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||||
|
|||||||
@@ -363,4 +363,24 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(track->getSampleRate(), 44100);
|
EXPECT_EQ(track->getSampleRate(), 44100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Track_comment)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getComment(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->setComment("MyComment");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getComment(), "MyComment");
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -273,6 +273,7 @@ namespace lms::metadata
|
|||||||
track.originalYear = utils::parseYear(*dateStr);
|
track.originalYear = utils::parseYear(*dateStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
track.comments = getTagValuesAs<std::string>(tagReader, TagType::Comment, {} /* no custom delimiter on comments */);
|
||||||
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
|
track.copyright = getTagValueAs<std::string>(tagReader, TagType::Copyright).value_or("");
|
||||||
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
|
||||||
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
|
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ namespace lms::metadata
|
|||||||
std::optional<core::UUID> acoustID;
|
std::optional<core::UUID> acoustID;
|
||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
|
std::vector<std::string> comments;
|
||||||
std::optional<float> replayGain;
|
std::optional<float> replayGain;
|
||||||
std::string artistDisplayName;
|
std::string artistDisplayName;
|
||||||
std::vector<Artist> artists;
|
std::vector<Artist> artists;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace lms::metadata
|
|||||||
{ TagType::AlbumArtist, { "MyAlbumArtist1 & MyAlbumArtist2" } },
|
{ TagType::AlbumArtist, { "MyAlbumArtist1 & MyAlbumArtist2" } },
|
||||||
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
|
{ TagType::AlbumArtists, { "MyAlbumArtist1", "MyAlbumArtist2" } },
|
||||||
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
|
{ TagType::AlbumArtistsSortOrder, { "MyAlbumArtist1SortName", "MyAlbumArtist2SortName" } },
|
||||||
|
{ TagType::Comment, { "Comment1", "Comment2" } },
|
||||||
{ TagType::Composer, { "MyComposer1", "MyComposer2" } },
|
{ TagType::Composer, { "MyComposer1", "MyComposer2" } },
|
||||||
{ TagType::ComposerSortOrder, { "MyComposerSortOrder1", "MyComposerSortOrder2" } },
|
{ TagType::ComposerSortOrder, { "MyComposerSortOrder1", "MyComposerSortOrder2" } },
|
||||||
{ TagType::Conductor, { "MyConductor1", "MyConductor2" } },
|
{ TagType::Conductor, { "MyConductor1", "MyConductor2" } },
|
||||||
@@ -103,6 +104,9 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artists[1].name, "MyArtist2");
|
EXPECT_EQ(track->artists[1].name, "MyArtist2");
|
||||||
EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName");
|
EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName");
|
||||||
EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
|
EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
|
||||||
|
ASSERT_EQ(track->comments.size(), 2);
|
||||||
|
EXPECT_EQ(track->comments[0], "Comment1");
|
||||||
|
EXPECT_EQ(track->comments[1], "Comment2");
|
||||||
ASSERT_EQ(track->composerArtists.size(), 2);
|
ASSERT_EQ(track->composerArtists.size(), 2);
|
||||||
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
|
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
|
||||||
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
|
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
|
||||||
|
|||||||
@@ -710,6 +710,7 @@ namespace lms::scanner
|
|||||||
track.modify()->setHasCover(trackMetadata->hasCover);
|
track.modify()->setHasCover(trackMetadata->hasCover);
|
||||||
track.modify()->setCopyright(trackMetadata->copyright);
|
track.modify()->setCopyright(trackMetadata->copyright);
|
||||||
track.modify()->setCopyrightURL(trackMetadata->copyrightURL);
|
track.modify()->setCopyrightURL(trackMetadata->copyrightURL);
|
||||||
|
track.modify()->setComment(!trackMetadata->comments.empty() ? trackMetadata->comments.front() : ""); // only take the first one for now
|
||||||
track.modify()->setTrackReplayGain(trackMetadata->replayGain);
|
track.modify()->setTrackReplayGain(trackMetadata->replayGain);
|
||||||
track.modify()->setArtistDisplayName(trackMetadata->artistDisplayName);
|
track.modify()->setArtistDisplayName(trackMetadata->artistDisplayName);
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ namespace lms::api::subsonic
|
|||||||
if (!context.enableOpenSubsonic)
|
if (!context.enableOpenSubsonic)
|
||||||
return trackResponse;
|
return trackResponse;
|
||||||
|
|
||||||
|
trackResponse.setAttribute("comment", track->getComment());
|
||||||
trackResponse.setAttribute("bitDepth", track->getBitsPerSample());
|
trackResponse.setAttribute("bitDepth", track->getBitsPerSample());
|
||||||
trackResponse.setAttribute("samplingRate", track->getSampleRate());
|
trackResponse.setAttribute("samplingRate", track->getSampleRate());
|
||||||
trackResponse.setAttribute("channelCount", track->getChannelCount());
|
trackResponse.setAttribute("channelCount", track->getChannelCount());
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ namespace lms::ui::TrackListHelpers
|
|||||||
{
|
{
|
||||||
std::unique_ptr<Wt::WContainerWidget> artistContainer{ utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) };
|
std::unique_ptr<Wt::WContainerWidget> artistContainer{ utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) };
|
||||||
auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
|
auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
|
||||||
artistsEntry->bindString("type", role);
|
artistsEntry->bindString("type", role, Wt::TextFormat::Plain);
|
||||||
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
||||||
artistTable->addWidget(std::move(artistsEntry));
|
artistTable->addWidget(std::move(artistsEntry));
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ namespace lms::ui::TrackListHelpers
|
|||||||
if (audioStream)
|
if (audioStream)
|
||||||
{
|
{
|
||||||
trackInfo->setCondition("if-has-codec", true);
|
trackInfo->setCondition("if-has-codec", true);
|
||||||
trackInfo->bindString("codec", audioStream->codecName);
|
trackInfo->bindString("codec", audioStream->codecName, Wt::TextFormat::Plain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +137,12 @@ namespace lms::ui::TrackListHelpers
|
|||||||
|
|
||||||
trackInfo->bindInt("playcount", core::Service<scrobbling::IScrobblingService>::get()->getCount(LmsApp->getUserId(), track->getId()));
|
trackInfo->bindInt("playcount", core::Service<scrobbling::IScrobblingService>::get()->getCount(LmsApp->getUserId(), track->getId()));
|
||||||
|
|
||||||
|
if (std::string_view comment{ track->getComment() }; !comment.empty())
|
||||||
|
{
|
||||||
|
trackInfo->setCondition("if-has-comment", true);
|
||||||
|
trackInfo->bindString("comment", Wt::WString::fromUTF8(std::string{ comment }), Wt::TextFormat::Plain);
|
||||||
|
}
|
||||||
|
|
||||||
Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", utils::createFilterClustersForTrack(track, filters)) };
|
Wt::WContainerWidget* clusterContainer{ trackInfo->bindWidget("clusters", utils::createFilterClustersForTrack(track, filters)) };
|
||||||
if (clusterContainer->count() > 0)
|
if (clusterContainer->count() > 0)
|
||||||
trackInfo->setCondition("if-has-clusters", true);
|
trackInfo->setCondition("if-has-clusters", true);
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ namespace lms::metadata
|
|||||||
if (!track->copyright.empty())
|
if (!track->copyright.empty())
|
||||||
std::cout << "Copyright: " << track->copyright << std::endl;
|
std::cout << "Copyright: " << track->copyright << std::endl;
|
||||||
|
|
||||||
|
for (const auto& comment : track->comments)
|
||||||
|
std::cout << "Comment: '" << comment << "'" << std::endl;
|
||||||
|
|
||||||
if (!track->copyrightURL.empty())
|
if (!track->copyrightURL.empty())
|
||||||
std::cout << "CopyrightURL: " << track->copyrightURL << std::endl;
|
std::cout << "CopyrightURL: " << track->copyrightURL << std::endl;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user