OpenSubsonic: changed the originalReleaseDate field format, fixes #400
This commit is contained in:
@@ -299,6 +299,16 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
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 migrateFromV49(Session& session)
|
||||||
|
{
|
||||||
|
// Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing)
|
||||||
|
session.getDboSession().execute("ALTER TABLE track ADD year INTEGER");
|
||||||
|
session.getDboSession().execute("ALTER TABLE track ADD original_year INTEGER");
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
void doDbMigration(Session& session)
|
void 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)" };
|
||||||
@@ -326,6 +336,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
|||||||
{46, migrateFromV46},
|
{46, migrateFromV46},
|
||||||
{47, migrateFromV47},
|
{47, migrateFromV47},
|
||||||
{48, migrateFromV48},
|
{48, migrateFromV48},
|
||||||
|
{49, migrateFromV49},
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Database
|
|||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
using Version = std::size_t;
|
using Version = std::size_t;
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 49 };
|
static constexpr Version LMS_DATABASE_VERSION{ 50 };
|
||||||
class VersionInfo
|
class VersionInfo
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ namespace Database
|
|||||||
|
|
||||||
if (params.dateRange)
|
if (params.dateRange)
|
||||||
{
|
{
|
||||||
query.where("t.date >= ?").bind(params.dateRange->begin);
|
query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) >= ?").bind(params.dateRange->begin);
|
||||||
query.where("t.date <= ?").bind(params.dateRange->end);
|
query.where("COALESCE(CAST(SUBSTR(t.date, 1, 4) AS INTEGER), t.year) <= ?").bind(params.dateRange->end);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (std::string_view keyword : params.keywords)
|
for (std::string_view keyword : params.keywords)
|
||||||
@@ -170,13 +170,13 @@ namespace Database
|
|||||||
query.orderBy("t.file_last_write DESC");
|
query.orderBy("t.file_last_write DESC");
|
||||||
break;
|
break;
|
||||||
case ReleaseSortMethod::Date:
|
case ReleaseSortMethod::Date:
|
||||||
query.orderBy("t.date, r.name COLLATE NOCASE");
|
query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)), r.name COLLATE NOCASE");
|
||||||
break;
|
break;
|
||||||
case ReleaseSortMethod::OriginalDate:
|
case ReleaseSortMethod::OriginalDate:
|
||||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
|
query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)), r.name COLLATE NOCASE");
|
||||||
break;
|
break;
|
||||||
case ReleaseSortMethod::OriginalDateDesc:
|
case ReleaseSortMethod::OriginalDateDesc:
|
||||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
|
query.orderBy("COALESCE(original_date, CAST(original_year AS TEXT), date, CAST(year AS TEXT)) DESC, r.name COLLATE NOCASE");
|
||||||
break;
|
break;
|
||||||
case ReleaseSortMethod::StarredDateDesc:
|
case ReleaseSortMethod::StarredDateDesc:
|
||||||
assert(params.starringUser.isValid());
|
assert(params.starringUser.isValid());
|
||||||
@@ -359,17 +359,17 @@ namespace Database
|
|||||||
return discs;
|
return discs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDate Release::getReleaseDate() const
|
Wt::WDate Release::getDate() const
|
||||||
{
|
{
|
||||||
return getReleaseDate(false);
|
return getDate(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDate Release::getOriginalReleaseDate() const
|
Wt::WDate Release::getOriginalDate() const
|
||||||
{
|
{
|
||||||
return getReleaseDate(true);
|
return getDate(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDate Release::getReleaseDate(bool original) const
|
Wt::WDate Release::getDate(bool original) const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -388,6 +388,35 @@ namespace Database
|
|||||||
|
|
||||||
return dates.front();
|
return dates.front();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<int> Release::getYear() const
|
||||||
|
{
|
||||||
|
return getYear(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> Release::getOriginalYear() const
|
||||||
|
{
|
||||||
|
return getYear(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> Release::getYear(bool original) const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
|
||||||
|
const char* field{ original ? "original_year" : "year" };
|
||||||
|
|
||||||
|
auto years{ session()->query<std::optional<int>>(
|
||||||
|
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||||
|
.where("r.id = ?").bind(getId())
|
||||||
|
.groupBy(field)
|
||||||
|
.resultList() };
|
||||||
|
|
||||||
|
// various years => invalid years
|
||||||
|
if (years.empty() || years.size() > 1)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
return years.front();
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<std::string> Release::getCopyright() const
|
std::optional<std::string> Release::getCopyright() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ namespace Database
|
|||||||
query.orderBy("t.name COLLATE NOCASE");
|
query.orderBy("t.name COLLATE NOCASE");
|
||||||
break;
|
break;
|
||||||
case TrackSortMethod::DateDescAndRelease:
|
case TrackSortMethod::DateDescAndRelease:
|
||||||
query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number");
|
query.orderBy("COALESCE(t.date, CAST(t.year AS TEXT)) DESC,t.release_id,t.disc_number,t.track_number");
|
||||||
break;
|
break;
|
||||||
case TrackSortMethod::Release:
|
case TrackSortMethod::Release:
|
||||||
query.orderBy("t.disc_number,t.track_number");
|
query.orderBy("t.disc_number,t.track_number");
|
||||||
@@ -382,16 +382,6 @@ namespace Database
|
|||||||
_clusters.insert(getDboPtr(cluster));
|
_clusters.insert(getDboPtr(cluster));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<int> Track::getYear() const
|
|
||||||
{
|
|
||||||
return (_date.isValid() ? std::make_optional<int>(_date.year()) : std::nullopt);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<int> Track::getOriginalYear() const
|
|
||||||
{
|
|
||||||
return (_originalDate.isValid() ? std::make_optional<int>(_originalDate.year()) : std::nullopt);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<std::string> Track::getCopyright() const
|
std::optional<std::string> Track::getCopyright() const
|
||||||
{
|
{
|
||||||
return _copyright != "" ? std::make_optional<std::string>(_copyright) : std::nullopt;
|
return _copyright != "" ? std::make_optional<std::string>(_copyright) : std::nullopt;
|
||||||
|
|||||||
@@ -23,30 +23,29 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
static const std::set<Bitrate> allowedAudioBitrates
|
static const std::set<Bitrate> allowedAudioBitrates
|
||||||
{
|
{
|
||||||
64000,
|
64000,
|
||||||
96000,
|
96000,
|
||||||
128000,
|
128000,
|
||||||
192000,
|
192000,
|
||||||
320000,
|
320000,
|
||||||
};
|
};
|
||||||
|
|
||||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)> func)
|
void visitAllowedAudioBitrates(std::function<void(Bitrate)> func)
|
||||||
{
|
{
|
||||||
for (Bitrate bitrate : allowedAudioBitrates)
|
for (Bitrate bitrate : allowedAudioBitrates)
|
||||||
func(bitrate);
|
func(bitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isAudioBitrateAllowed(Bitrate bitrate)
|
bool isAudioBitrateAllowed(Bitrate bitrate)
|
||||||
{
|
{
|
||||||
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
|
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateRange
|
DateRange DateRange::fromYearRange(int from, int to)
|
||||||
DateRange::fromYearRange(int from, int to)
|
{
|
||||||
{
|
return DateRange{ from, to };
|
||||||
return DateRange {{from, 1, 1}, {to, 12, 31}};
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,8 +132,10 @@ namespace Database
|
|||||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||||
|
|
||||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||||
Wt::WDate getReleaseDate() const;
|
Wt::WDate getDate() const;
|
||||||
Wt::WDate getOriginalReleaseDate() const;
|
std::optional<int> getYear() const;
|
||||||
|
Wt::WDate getOriginalDate() const;
|
||||||
|
std::optional<int> getOriginalYear() const;
|
||||||
std::optional<std::string> getCopyright() const;
|
std::optional<std::string> getCopyright() const;
|
||||||
std::optional<std::string> getCopyrightURL() const;
|
std::optional<std::string> getCopyrightURL() const;
|
||||||
std::size_t getMeanBitrate() const;
|
std::size_t getMeanBitrate() const;
|
||||||
@@ -181,7 +183,8 @@ namespace Database
|
|||||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||||
|
|
||||||
Wt::WDate getReleaseDate(bool original) const;
|
Wt::WDate getDate(bool original) const;
|
||||||
|
std::optional<int> getYear(bool original) const;
|
||||||
|
|
||||||
static constexpr std::size_t _maxNameLength{ 128 };
|
static constexpr std::size_t _maxNameLength{ 128 };
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,9 @@ namespace Database {
|
|||||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||||
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
||||||
void setDate(const Wt::WDate& date) { _date = date; }
|
void setDate(const Wt::WDate& date) { _date = date; }
|
||||||
|
void setYear(std::optional<int> year) { _year = year; }
|
||||||
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
||||||
|
void setOriginalYear(std::optional<int> year) { _originalYear = year; }
|
||||||
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
||||||
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||||
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
||||||
@@ -156,8 +158,10 @@ namespace Database {
|
|||||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||||
std::size_t getBitrate() const { return _bitrate; }
|
std::size_t getBitrate() const { return _bitrate; }
|
||||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||||
std::optional<int> getYear() const;
|
const Wt::WDate& getDate() const { return _date; }
|
||||||
std::optional<int> getOriginalYear() const;
|
std::optional<int> getYear() const { return _year; }
|
||||||
|
const Wt::WDate& getOriginalDate() const { return _originalDate; }
|
||||||
|
std::optional<int> getOriginalYear() const { return _originalYear; };
|
||||||
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
||||||
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
||||||
bool hasCover() const { return _hasCover; }
|
bool hasCover() const { return _hasCover; }
|
||||||
@@ -190,7 +194,9 @@ namespace Database {
|
|||||||
Wt::Dbo::field(a, _duration, "duration");
|
Wt::Dbo::field(a, _duration, "duration");
|
||||||
Wt::Dbo::field(a, _bitrate, "bitrate");
|
Wt::Dbo::field(a, _bitrate, "bitrate");
|
||||||
Wt::Dbo::field(a, _date, "date");
|
Wt::Dbo::field(a, _date, "date");
|
||||||
|
Wt::Dbo::field(a, _year, "year");
|
||||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||||
|
Wt::Dbo::field(a, _originalYear, "original_year");
|
||||||
Wt::Dbo::field(a, _filePath, "file_path");
|
Wt::Dbo::field(a, _filePath, "file_path");
|
||||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||||
Wt::Dbo::field(a, _fileAdded, "file_added");
|
Wt::Dbo::field(a, _fileAdded, "file_added");
|
||||||
@@ -225,7 +231,9 @@ namespace Database {
|
|||||||
std::chrono::duration<int, std::milli> _duration{};
|
std::chrono::duration<int, std::milli> _duration{};
|
||||||
int _bitrate; // in bps
|
int _bitrate; // in bps
|
||||||
Wt::WDate _date;
|
Wt::WDate _date;
|
||||||
|
std::optional<int> _year;
|
||||||
Wt::WDate _originalDate;
|
Wt::WDate _originalDate;
|
||||||
|
std::optional<int> _originalYear;
|
||||||
std::string _filePath;
|
std::string _filePath;
|
||||||
Wt::WDateTime _fileLastWrite;
|
Wt::WDateTime _fileLastWrite;
|
||||||
Wt::WDateTime _fileAdded;
|
Wt::WDateTime _fileAdded;
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ namespace Database
|
|||||||
|
|
||||||
struct DateRange
|
struct DateRange
|
||||||
{
|
{
|
||||||
Wt::WDate begin;
|
int begin;
|
||||||
Wt::WDate end;
|
int end;
|
||||||
|
|
||||||
static DateRange fromYearRange(int from, int to);
|
static DateRange fromYearRange(int from, int to);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -402,8 +402,61 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
|||||||
track1A.get().modify()->setOriginalDate(release1OriginalDate);
|
track1A.get().modify()->setOriginalDate(release1OriginalDate);
|
||||||
track1B.get().modify()->setOriginalDate(release1OriginalDate);
|
track1B.get().modify()->setOriginalDate(release1OriginalDate);
|
||||||
|
|
||||||
EXPECT_EQ(release1.get()->getReleaseDate(), release1Date);
|
EXPECT_EQ(release1.get()->getDate(), release1Date);
|
||||||
EXPECT_EQ(release1.get()->getOriginalReleaseDate(), release1OriginalDate);
|
EXPECT_EQ(release1.get()->getOriginalDate(), release1OriginalDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000))) };
|
||||||
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||||
|
|
||||||
|
releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1994, 1994)));
|
||||||
|
ASSERT_EQ(releases.results.size(), 1);
|
||||||
|
EXPECT_EQ(releases.results.front(), release1.getId());
|
||||||
|
|
||||||
|
releases = Release::findIds(session, Release::FindParameters{}.setDateRange(DateRange::fromYearRange(1993, 1993)));
|
||||||
|
ASSERT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, MultiTracksSingleReleaseYear)
|
||||||
|
{
|
||||||
|
ScopedRelease release1{ session, "MyRelease1" };
|
||||||
|
ScopedRelease release2{ session, "MyRelease2" };
|
||||||
|
const int release1Year{ 1994 };
|
||||||
|
const int release1OriginalYear{ 1993 };
|
||||||
|
|
||||||
|
ScopedTrack track1A{ session, "MyTrack1A" };
|
||||||
|
ScopedTrack track1B{ session, "MyTrack1B" };
|
||||||
|
ScopedTrack track2A{ session, "MyTrack2A" };
|
||||||
|
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000))) };
|
||||||
|
EXPECT_EQ(releases.results.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
track1A.get().modify()->setRelease(release1.get());
|
||||||
|
track1B.get().modify()->setRelease(release1.get());
|
||||||
|
track2A.get().modify()->setRelease(release2.get());
|
||||||
|
track2B.get().modify()->setRelease(release2.get());
|
||||||
|
|
||||||
|
|
||||||
|
track1A.get().modify()->setYear(release1Year);
|
||||||
|
track1B.get().modify()->setYear(release1Year);
|
||||||
|
track1A.get().modify()->setOriginalYear(release1OriginalYear);
|
||||||
|
track1B.get().modify()->setOriginalYear(release1OriginalYear);
|
||||||
|
|
||||||
|
EXPECT_EQ(release1.get()->getYear(), release1Year);
|
||||||
|
EXPECT_EQ(release1.get()->getOriginalYear(), release1OriginalYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
|||||||
TEST_F(DatabaseFixture, Track_date)
|
TEST_F(DatabaseFixture, Track_date)
|
||||||
{
|
{
|
||||||
ScopedTrack track{ session, "MyTrack" };
|
ScopedTrack track{ session, "MyTrack" };
|
||||||
|
const Wt::WDate date{ 1995, 5, 5 };
|
||||||
|
const Wt::WDate originalDate{ 1994, 2, 2 };
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
EXPECT_EQ(track->getYear(), std::nullopt);
|
EXPECT_EQ(track->getYear(), std::nullopt);
|
||||||
@@ -135,14 +136,28 @@ TEST_F(DatabaseFixture, Track_date)
|
|||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
|
track.get().modify()->setDate(date);
|
||||||
track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
|
track.get().modify()->setOriginalDate(originalDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
EXPECT_EQ(track->getYear(), 1995);
|
EXPECT_EQ(track->getYear(), std::nullopt);
|
||||||
EXPECT_EQ(track->getOriginalYear(), 1994);
|
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
|
||||||
|
EXPECT_EQ(track->getDate(), date);
|
||||||
|
EXPECT_EQ(track->getOriginalDate(), originalDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->setYear(date.year());
|
||||||
|
track.get().modify()->setOriginalYear(originalDate.year());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getYear(), date.year());
|
||||||
|
EXPECT_EQ(track->getOriginalYear(), originalDate.year());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -333,26 +333,22 @@ namespace MetaData
|
|||||||
}
|
}
|
||||||
else if (tag == "DATE")
|
else if (tag == "DATE")
|
||||||
{
|
{
|
||||||
// Higher priority than YEAR
|
|
||||||
if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
|
if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
|
||||||
track.date = date;
|
track.date = date;
|
||||||
|
else if (!track.year)
|
||||||
|
track.year = StringUtils::readAs<int>(value);
|
||||||
}
|
}
|
||||||
else if (tag == "YEAR" && !track.date.isValid())
|
else if (tag == "YEAR")
|
||||||
{
|
track.year = StringUtils::readAs<int>(value);
|
||||||
// lower priority than DATE
|
|
||||||
track.date = Utils::parseDate(value);
|
|
||||||
}
|
|
||||||
else if (tag == "ORIGINALDATE")
|
else if (tag == "ORIGINALDATE")
|
||||||
{
|
{
|
||||||
// Higher priority than ORIGINALYEAR
|
|
||||||
if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
|
if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
|
||||||
track.originalDate = date;
|
track.originalDate = date;
|
||||||
|
else if (!track.originalYear)
|
||||||
|
track.originalYear = StringUtils::readAs<int>(value);
|
||||||
}
|
}
|
||||||
else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid())
|
else if (tag == "ORIGINALYEAR")
|
||||||
{
|
track.originalYear = StringUtils::readAs<int>(value);
|
||||||
// Lower priority than ORIGINALDATE
|
|
||||||
track.originalDate = Utils::parseDate(value);
|
|
||||||
}
|
|
||||||
else if (tag == "METADATA_BLOCK_PICTURE")
|
else if (tag == "METADATA_BLOCK_PICTURE")
|
||||||
track.hasCover = true;
|
track.hasCover = true;
|
||||||
else if (tag == "COPYRIGHT")
|
else if (tag == "COPYRIGHT")
|
||||||
@@ -510,6 +506,14 @@ namespace MetaData
|
|||||||
for (const auto& [tag, values] : tags)
|
for (const auto& [tag, values] : tags)
|
||||||
processTag(track, tag, values, debug);
|
processTag(track, tag, values, debug);
|
||||||
|
|
||||||
|
// If a file has date but no year, set it
|
||||||
|
if (!track.year && track.date.isValid())
|
||||||
|
track.year = track.date.year();
|
||||||
|
|
||||||
|
// If a file has originalDate but no originalYear, set it
|
||||||
|
if (!track.originalYear && track.originalDate.isValid())
|
||||||
|
track.originalYear = track.originalDate.year();
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,17 +37,23 @@ namespace MetaData::Utils
|
|||||||
|
|
||||||
for (const char* format : formats)
|
for (const char* format : formats)
|
||||||
{
|
{
|
||||||
std::tm tm = {};
|
std::tm tm{};
|
||||||
|
tm.tm_mon = -1;
|
||||||
|
tm.tm_mday = -1;
|
||||||
|
|
||||||
std::istringstream ss{ std::string {dateStr} }; // TODO, remove extra copy here
|
std::istringstream ss{ std::string {dateStr} }; // TODO, remove extra copy here
|
||||||
ss >> std::get_time(&tm, format);
|
ss >> std::get_time(&tm, format);
|
||||||
if (ss.fail())
|
if (ss.fail())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (tm.tm_mday <= 0 || tm.tm_mon < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
const Wt::WDate res
|
const Wt::WDate res
|
||||||
{
|
{
|
||||||
tm.tm_year + 1900, // years since 1900
|
tm.tm_year + 1900, // tm.tm_year: years since 1900
|
||||||
tm.tm_mon + 1, // months since January – [0, 11]
|
tm.tm_mon + 1, // tm.tm_mon: months since January – [00, 11]
|
||||||
tm.tm_mday ? tm.tm_mday : 1 // day of the month – [1, 31]
|
tm.tm_mday // tm.tm_mday: day of the month – [1, 31]
|
||||||
};
|
};
|
||||||
if (!res.isValid())
|
if (!res.isValid())
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ namespace MetaData
|
|||||||
Tags userExtraTags;
|
Tags userExtraTags;
|
||||||
std::chrono::milliseconds duration{};
|
std::chrono::milliseconds duration{};
|
||||||
std::size_t bitrate{};
|
std::size_t bitrate{};
|
||||||
|
std::optional<int> year{};
|
||||||
Wt::WDate date;
|
Wt::WDate date;
|
||||||
|
std::optional<int> originalYear{};
|
||||||
Wt::WDate originalDate;
|
Wt::WDate originalDate;
|
||||||
bool hasCover{};
|
bool hasCover{};
|
||||||
std::optional<UUID> acoustID;
|
std::optional<UUID> acoustID;
|
||||||
|
|||||||
@@ -24,94 +24,98 @@
|
|||||||
|
|
||||||
TEST(MetaData, parseDate)
|
TEST(MetaData, parseDate)
|
||||||
{
|
{
|
||||||
using namespace MetaData::Utils;
|
using namespace MetaData::Utils;
|
||||||
|
|
||||||
struct TestCase
|
struct TestCase
|
||||||
{
|
{
|
||||||
std::string str;
|
std::string str;
|
||||||
Wt::WDate result;
|
Wt::WDate result;
|
||||||
} testCases []
|
} testCases[]
|
||||||
{
|
{
|
||||||
{ "1995-05-09", Wt::WDate {1995, 5, 9} },
|
{ "1995-05-09", Wt::WDate {1995, 5, 9} },
|
||||||
{ "1995-01-01", Wt::WDate {1995, 1, 1} },
|
{ "1995-01-01", Wt::WDate {1995, 1, 1} },
|
||||||
{ "1900-01-01", Wt::WDate {1900, 1, 1} },
|
{ "1900-01-01", Wt::WDate {1900, 1, 1} },
|
||||||
{ "1899-01-01", Wt::WDate {1899, 1, 1} },
|
{ "1899-01-01", Wt::WDate {1899, 1, 1} },
|
||||||
{ "1899-12-31", Wt::WDate {1899, 12, 31} },
|
{ "1899-12-31", Wt::WDate {1899, 12, 31} },
|
||||||
{ "1899-11-30", Wt::WDate {1899, 11, 30} },
|
{ "1899-11-30", Wt::WDate {1899, 11, 30} },
|
||||||
{ "1500-11-30", Wt::WDate {1500, 11, 30} },
|
{ "1500-11-30", Wt::WDate {1500, 11, 30} },
|
||||||
{ "1000-11-30", Wt::WDate {1000, 11, 30} },
|
{ "1000-11-30", Wt::WDate {1000, 11, 30} },
|
||||||
{ "1899-11-31", Wt::WDate {} }, // invalid day
|
{ "1899-11-31", Wt::WDate {} }, // invalid day
|
||||||
{ "1899-13-01", Wt::WDate {} }, // invalid month
|
{ "1899-11-00", Wt::WDate {} }, // invalid day
|
||||||
{ "1899-11", Wt::WDate {1899, 11, 1} }, // missing day
|
{ "1899-13-01", Wt::WDate {} }, // invalid month
|
||||||
{ "1899", Wt::WDate {1899, 1, 1} }, // missing month and days
|
{ "1899-00-01", Wt::WDate {} }, // invalid month
|
||||||
{ "1600", Wt::WDate {1600, 1, 1} }, // missing month and days
|
{ "1899-11", Wt::WDate {} }, // missing day
|
||||||
{ "1995/05/09", Wt::WDate {1995, 5, 9} },
|
{ "1899", Wt::WDate {} }, // missing month and days
|
||||||
{ "1995/01/01", Wt::WDate {1995, 1, 1} },
|
{ "1600", Wt::WDate {} }, // missing month and days
|
||||||
{ "1900/01/01", Wt::WDate {1900, 1, 1} },
|
{ "1995/05/09", Wt::WDate {1995, 5, 9} },
|
||||||
{ "1899/01/01", Wt::WDate {1899, 1, 1} },
|
{ "1995/01/01", Wt::WDate {1995, 1, 1} },
|
||||||
{ "1899/12/31", Wt::WDate {1899, 12, 31} },
|
{ "1900/01/01", Wt::WDate {1900, 1, 1} },
|
||||||
{ "1899/11/30", Wt::WDate {1899, 11, 30} },
|
{ "1899/01/01", Wt::WDate {1899, 1, 1} },
|
||||||
{ "1500/11/30", Wt::WDate {1500, 11, 30} },
|
{ "1899/12/31", Wt::WDate {1899, 12, 31} },
|
||||||
{ "1000/11/30", Wt::WDate {1000, 11, 30} },
|
{ "1899/11/30", Wt::WDate {1899, 11, 30} },
|
||||||
{ "1899/11/31", Wt::WDate {} }, // invalid day
|
{ "1500/11/30", Wt::WDate {1500, 11, 30} },
|
||||||
{ "1899/13/01", Wt::WDate {} }, // invalid month
|
{ "1000/11/30", Wt::WDate {1000, 11, 30} },
|
||||||
{ "1899/11", Wt::WDate {1899, 11, 1} }, // missing day
|
{ "1899/11/31", Wt::WDate {} }, // invalid day
|
||||||
{ "1899", Wt::WDate {1899, 1, 1} }, // missing month and days
|
{ "1899/11/00", Wt::WDate {} }, // invalid day
|
||||||
{ "1600", Wt::WDate {1600, 1, 1} }, // missing month and days
|
{ "1899/13/01", Wt::WDate {} }, // invalid month
|
||||||
{ "1995/05-09", Wt::WDate {} }, // invalid mixup separators
|
{ "1899/00/01", Wt::WDate {} }, // invalid month
|
||||||
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators
|
{ "1899/11", Wt::WDate {} }, // missing day
|
||||||
};
|
{ "1899", Wt::WDate {} }, // missing month and days
|
||||||
|
{ "1600", Wt::WDate {} }, // missing month and days
|
||||||
|
{ "1995/05-09", Wt::WDate {} }, // invalid mixup separators
|
||||||
|
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators
|
||||||
|
};
|
||||||
|
|
||||||
for (const TestCase& testCase : testCases)
|
for (const TestCase& testCase : testCases)
|
||||||
{
|
{
|
||||||
const Wt::WDate parsed {parseDate(testCase.str)};
|
const Wt::WDate parsed{ parseDate(testCase.str) };
|
||||||
|
|
||||||
EXPECT_EQ(parsed.year(), testCase.result.year()) << " str was '" << testCase.str << "'";
|
EXPECT_EQ(parsed.year(), testCase.result.year()) << " str was '" << testCase.str << "'";
|
||||||
EXPECT_EQ(parsed.month(), testCase.result.month()) << " str was '" << testCase.str << "'";
|
EXPECT_EQ(parsed.month(), testCase.result.month()) << " str was '" << testCase.str << "'";
|
||||||
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
|
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(MetaData, extractPerformerAndRole)
|
TEST(MetaData, extractPerformerAndRole)
|
||||||
{
|
{
|
||||||
using namespace MetaData::Utils;
|
using namespace MetaData::Utils;
|
||||||
|
|
||||||
struct TestCase
|
struct TestCase
|
||||||
{
|
{
|
||||||
std::string str;
|
std::string str;
|
||||||
std::string expectedArtistName;
|
std::string expectedArtistName;
|
||||||
std::string expectedRole;
|
std::string expectedRole;
|
||||||
} testCases []
|
} testCases[]
|
||||||
{
|
{
|
||||||
{ "", "", "" },
|
{ "", "", "" },
|
||||||
{ "(myrole)", "", "myrole" },
|
{ "(myrole)", "", "myrole" },
|
||||||
{ "(my role)", "", "my role" },
|
{ "(my role)", "", "my role" },
|
||||||
{ " ( my role ) ", "", "my role" },
|
{ " ( my role ) ", "", "my role" },
|
||||||
{ " (()) ", "", "()" },
|
{ " (()) ", "", "()" },
|
||||||
{ ")", ")", "" },
|
{ ")", ")", "" },
|
||||||
{ "(", "(", "" },
|
{ "(", "(", "" },
|
||||||
{ "artist name (my role)", "artist name", "my role" },
|
{ "artist name (my role)", "artist name", "my role" },
|
||||||
{ "artist name ()", "artist name", "" },
|
{ "artist name ()", "artist name", "" },
|
||||||
{ "artist name ( )", "artist name", "" },
|
{ "artist name ( )", "artist name", "" },
|
||||||
{ "artist (subname) name", "artist (subname) name", "" },
|
{ "artist (subname) name", "artist (subname) name", "" },
|
||||||
{ " artist name ( my role )", "artist name", "my role" },
|
{ " artist name ( my role )", "artist name", "my role" },
|
||||||
{ "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" },
|
{ "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" },
|
||||||
{ "artist name", "artist name", "" },
|
{ "artist name", "artist name", "" },
|
||||||
{ " artist name ", "artist name", "" },
|
{ " artist name ", "artist name", "" },
|
||||||
{ "artist name (", "artist name (", "" },
|
{ "artist name (", "artist name (", "" },
|
||||||
{ "artist name )", "artist name )", "" },
|
{ "artist name )", "artist name )", "" },
|
||||||
{ "artist name (()", "artist name (", "" },
|
{ "artist name (()", "artist name (", "" },
|
||||||
{ "artist name (())", "artist name", "()" },
|
{ "artist name (())", "artist name", "()" },
|
||||||
{ "artist name ( () )", "artist name", "()" },
|
{ "artist name ( () )", "artist name", "()" },
|
||||||
{ "artist name (drums (drum set))", "artist name", "drums (drum set)" },
|
{ "artist name (drums (drum set))", "artist name", "drums (drum set)" },
|
||||||
{ "artist name ( drums (drum set) )", "artist name", "drums (drum set)" },
|
{ "artist name ( drums (drum set) )", "artist name", "drums (drum set)" },
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const TestCase& testCase : testCases)
|
for (const TestCase& testCase : testCases)
|
||||||
{
|
{
|
||||||
PerformerArtist performer {extractPerformerAndRole(testCase.str)};
|
PerformerArtist performer{ extractPerformerAndRole(testCase.str) };
|
||||||
|
|
||||||
EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'";
|
EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'";
|
||||||
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
|
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -443,11 +443,17 @@ namespace Scanner
|
|||||||
track.modify()->setTrackNumber(trackInfo->position);
|
track.modify()->setTrackNumber(trackInfo->position);
|
||||||
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
|
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
|
||||||
track.modify()->setDate(trackInfo->date);
|
track.modify()->setDate(trackInfo->date);
|
||||||
|
track.modify()->setYear(trackInfo->year);
|
||||||
track.modify()->setOriginalDate(trackInfo->originalDate);
|
track.modify()->setOriginalDate(trackInfo->originalDate);
|
||||||
|
track.modify()->setOriginalYear(trackInfo->originalYear);
|
||||||
|
|
||||||
// If a file has an OriginalYear but no Year, set it to ease filtering
|
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||||
if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid())
|
if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid())
|
||||||
track.modify()->setDate(trackInfo->originalDate);
|
track.modify()->setDate(trackInfo->originalDate);
|
||||||
|
|
||||||
|
// If a file has an OriginalYear but no Year, set it to ease filtering
|
||||||
|
if (!trackInfo->year && trackInfo->originalYear)
|
||||||
|
track.modify()->setYear(trackInfo->originalYear);
|
||||||
|
|
||||||
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
|
track.modify()->setRecordingMBID(trackInfo->recordingMBID);
|
||||||
track.modify()->setTrackMBID(trackInfo->mbid);
|
track.modify()->setTrackMBID(trackInfo->mbid);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ add_library(lmssubsonic SHARED
|
|||||||
impl/responses/Bookmark.cpp
|
impl/responses/Bookmark.cpp
|
||||||
impl/responses/Contributor.cpp
|
impl/responses/Contributor.cpp
|
||||||
impl/responses/DiscTitle.cpp
|
impl/responses/DiscTitle.cpp
|
||||||
|
impl/responses/ItemDate.cpp
|
||||||
impl/responses/ItemGenre.cpp
|
impl/responses/ItemGenre.cpp
|
||||||
impl/responses/Genre.cpp
|
impl/responses/Genre.cpp
|
||||||
impl/responses/Playlist.cpp
|
impl/responses/Playlist.cpp
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ namespace API::Subsonic
|
|||||||
};
|
};
|
||||||
|
|
||||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
||||||
static inline constexpr std::string_view serverVersion{ "5" };
|
static inline constexpr std::string_view serverVersion{ "6" };
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
#include "responses/Artist.hpp"
|
#include "responses/Artist.hpp"
|
||||||
#include "responses/DiscTitle.hpp"
|
#include "responses/DiscTitle.hpp"
|
||||||
|
#include "responses/ItemDate.hpp"
|
||||||
#include "responses/ItemGenre.hpp"
|
#include "responses/ItemGenre.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
@@ -58,8 +59,8 @@ namespace API::Subsonic
|
|||||||
albumNode.setAttribute("created", StringUtils::toISO8601String(release->getLastWritten()));
|
albumNode.setAttribute("created", StringUtils::toISO8601String(release->getLastWritten()));
|
||||||
albumNode.setAttribute("id", idToString(release->getId()));
|
albumNode.setAttribute("id", idToString(release->getId()));
|
||||||
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
||||||
if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid())
|
if (const auto year{ release->getYear() })
|
||||||
albumNode.setAttribute("year", releaseDate.year());
|
albumNode.setAttribute("year", *year);
|
||||||
|
|
||||||
auto artists{ release->getReleaseArtists() };
|
auto artists{ release->getReleaseArtists() };
|
||||||
if (artists.empty())
|
if (artists.empty())
|
||||||
@@ -149,11 +150,7 @@ namespace API::Subsonic
|
|||||||
albumNode.addArrayChild("artists", createArtistNode(artist));
|
albumNode.addArrayChild("artists", createArtistNode(artist));
|
||||||
|
|
||||||
albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
|
albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
|
||||||
|
albumNode.addChild("originalReleaseDate", createItemDateNode(release->getOriginalDate(), release->getOriginalYear()));
|
||||||
{
|
|
||||||
const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() };
|
|
||||||
albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
bool isCompilation{};
|
bool isCompilation{};
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Genre.hpp"
|
||||||
|
|
||||||
|
#include "database/Cluster.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createItemDateNode(const Wt::WDate& date, std::optional<int> year)
|
||||||
|
{
|
||||||
|
Response::Node itemDateNode;
|
||||||
|
|
||||||
|
if (date.isValid())
|
||||||
|
{
|
||||||
|
itemDateNode.setAttribute("year", date.year());
|
||||||
|
itemDateNode.setAttribute("month", date.month());
|
||||||
|
itemDateNode.setAttribute("day", date.day());
|
||||||
|
}
|
||||||
|
else if (year)
|
||||||
|
{
|
||||||
|
itemDateNode.setAttribute("year", *year);
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemDateNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Wt/WDate.h>
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createItemDateNode(const Wt::WDate& date, std::optional<int> year);
|
||||||
|
}
|
||||||
@@ -155,7 +155,7 @@ TEST(StringUtils, escapeString)
|
|||||||
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
EXPECT_EQ(StringUtils::escapeString("**||", "*|", '_'), "_*_*_|_|");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(StringUtils, readAs)
|
TEST(StringUtils, readAs_bool)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
|
EXPECT_EQ(StringUtils::readAs<bool>("true"), true);
|
||||||
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
|
EXPECT_EQ(StringUtils::readAs<bool>("1"), true);
|
||||||
@@ -165,6 +165,19 @@ TEST(StringUtils, readAs)
|
|||||||
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(StringUtils, readAs_int)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("1024"), 1024);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("0"), 0);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("-0"), 0);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("-1"), -1);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>(""), std::nullopt);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("a"), std::nullopt);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("1024-1"), 1024);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("1024-"), 1024);
|
||||||
|
EXPECT_EQ(StringUtils::readAs<int>("1024/5"), 1024);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(StringUtils, capitalize)
|
TEST(StringUtils, capitalize)
|
||||||
{
|
{
|
||||||
struct TestCase
|
struct TestCase
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
|
|
||||||
if (showYear)
|
if (showYear)
|
||||||
{
|
{
|
||||||
Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) };
|
Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) };
|
||||||
if (!year.empty())
|
if (!year.empty())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-year", true);
|
entry->setCondition("if-has-year", true);
|
||||||
@@ -136,18 +136,18 @@ namespace UserInterface::ReleaseHelpers
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate)
|
Wt::WString buildReleaseYearString(std::optional<int> year, std::optional<int> originalYear)
|
||||||
{
|
{
|
||||||
Wt::WString res;
|
Wt::WString res;
|
||||||
|
|
||||||
// Year can be here, but originalYear can't be here without year (enforced by scanner)
|
// Year can be here, but originalYear can't be here without year (enforced by scanner)
|
||||||
if (!releaseDate.isValid())
|
if (!year)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
if (originalReleaseDate.isValid() && originalReleaseDate.year() != releaseDate.year())
|
if (originalYear && *originalYear != *year)
|
||||||
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
|
res = std::to_string(*originalYear) + " (" + std::to_string(*year) + ")";
|
||||||
else
|
else
|
||||||
res = std::to_string(releaseDate.year());
|
res = std::to_string(*year);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,5 +45,5 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
namespace UserInterface::ReleaseHelpers
|
namespace UserInterface::ReleaseHelpers
|
||||||
{
|
{
|
||||||
Wt::WString buildReleaseTypeString(const ReleaseType& releaseType);
|
Wt::WString buildReleaseTypeString(const ReleaseType& releaseType);
|
||||||
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate);
|
Wt::WString buildReleaseYearString(std::optional<int> year, std::optional<int> originalYear);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ namespace UserInterface
|
|||||||
|
|
||||||
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate()) };
|
Wt::WString year{ ReleaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) };
|
||||||
if (!year.empty())
|
if (!year.empty())
|
||||||
{
|
{
|
||||||
setCondition("if-has-year", true);
|
setCondition("if-has-year", true);
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ namespace UserInterface
|
|||||||
{
|
{
|
||||||
std::string releaseName;
|
std::string releaseName;
|
||||||
|
|
||||||
if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid())
|
if (const auto year{ release->getYear() })
|
||||||
releaseName += std::to_string(releaseDate.year()) + " - ";
|
releaseName += std::to_string(*year) + " - ";
|
||||||
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
||||||
|
|
||||||
return releaseName;
|
return releaseName;
|
||||||
|
|||||||
@@ -172,9 +172,13 @@ namespace
|
|||||||
|
|
||||||
if (track->date.isValid())
|
if (track->date.isValid())
|
||||||
std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl;
|
std::cout << "Date: " << track->date.toString("yyyy-MM-dd") << std::endl;
|
||||||
|
if (track->year)
|
||||||
|
std::cout << "Year: " << *track->year << std::endl;
|
||||||
|
|
||||||
if (track->originalDate.isValid())
|
if (track->originalDate.isValid())
|
||||||
std::cout << "Original date: " << track->originalDate.toString("yyyy-MM-dd") << std::endl;
|
std::cout << "Original date: " << track->originalDate.toString("yyyy-MM-dd") << std::endl;
|
||||||
|
if (track->originalYear)
|
||||||
|
std::cout << "Original year: " << *track->originalYear << std::endl;
|
||||||
|
|
||||||
std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl;
|
std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user