summaryrefslogtreecommitdiff
path: root/src/tangara/database/track.hpp
blob: d603945120d73466a3b547988e4809933994cfcb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
/*
 * Copyright 2023 jacqueline <me@jacqueline.id.au>
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */

#pragma once

#include <cstdint>

#include <map>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>

#include "leveldb/db.h"
#include "memory_resource.hpp"

namespace database {

/*
 * Uniquely describes a single track within the database. This value will be
 * consistent across database updates, and should ideally (but is not guaranteed
 * to) endure even across a track being removed and re-added.
 *
 * Four billion tracks should be enough for anybody.
 */
typedef uint32_t TrackId;

/*
 * Audio file encodings that we are aware of. Used to select an appropriate
 * decoder at play time.
 *
 * Values of this enum are persisted in this database, so it is probably never a
 * good idea to change the int representation of an existing value.
 */
enum class Container {
  kUnsupported = 0,
  kMp3 = 1,
  kWav = 2,
  kOgg = 3,
  kFlac = 4,
  kOpus = 5,
  kWavPack = 6,
};

enum class MediaType {
  // We don't know what this is.
  kUnknown = 0,
  // It's music! You know what music is. Usually short-form, but sometimes
  // long-form, since this type includes everything from standalone tracks, to
  // tracks within an album, to live set and event recordings.
  kMusic = 1,
  // Usually long usually primarily spoken content. One file per episode, and
  // the tagging norms for each file are all over the place. Prefer looking up
  // track tags from accompanying RSS feed files.
  kPodcast = 2,
  // Usually long usually primarily spoken content. One distinct piece of
  // 'audiobook' media may be split across several files, with a playlist or
  // cue file to tie the pieces back together. May also just be one big mp3.
  kAudiobook = 3,
};

enum class Tag {
  kTitle = 0,
  kArtist = 1,
  kAlbum = 2,
  kAlbumArtist = 3,
  kDisc = 4,
  kTrack = 5,
  kAlbumOrder = 6,
  kGenres = 7,
  kAllArtists = 8,
};

using TagValue = std::variant<std::monostate,
                              std::pmr::string,
                              uint32_t,
                              std::span<const std::pmr::string>>;

auto tagName(Tag) -> std::string;
auto tagHash(const TagValue&) -> uint64_t;
auto tagToString(const TagValue&) -> std::string;

/*
 * Owning container for tag-related track metadata that was extracted from a
 * file.
 */
class TrackTags {
 public:
  static auto create() -> std::shared_ptr<TrackTags>;

  TrackTags()
      : encoding_(Container::kUnsupported), genres_(&memory::kSpiRamResource) {}

  TrackTags(const TrackTags& other) = delete;
  TrackTags& operator=(TrackTags& other) = delete;

  bool operator==(const TrackTags&) const = default;

  auto get(Tag) const -> TagValue;
  auto set(Tag, std::string_view) -> void;

  auto allPresent() const -> std::vector<Tag>;

  auto encoding() const -> Container { return encoding_; };
  auto encoding(Container e) -> void { encoding_ = e; };

  auto title() const -> const std::optional<std::pmr::string>&;
  auto title(std::string_view) -> void;

  auto artist() const -> const std::optional<std::pmr::string>&;
  auto artist(std::string_view) -> void;

  auto allArtists() const -> std::span<const std::pmr::string>;
  auto allArtists(const std::string_view) -> void;
  auto singleAllArtists(const std::string_view) -> void;

  auto album() const -> const std::optional<std::pmr::string>&;
  auto album(std::string_view) -> void;

  auto albumArtist() const -> const std::optional<std::pmr::string>&;
  auto albumArtist(std::string_view) -> void;

  auto disc() const -> const std::optional<uint8_t>&;
  auto disc(const std::string_view) -> void;

  auto track() const -> const std::optional<uint16_t>&;
  auto track(const std::string_view) -> void;

  auto albumOrder() const -> uint32_t;

  auto genres() const -> std::span<const std::pmr::string>;
  auto genres(const std::string_view) -> void;

  /*
   * Returns a hash of the 'identifying' tags of this track. That is, a hash
   * that can be used to determine if one track is likely the same as another,
   * across things like re-encoding, re-mastering, or moving the underlying
   * file.
   */
  auto Hash() const -> uint64_t;

 private:
  Container encoding_;

  std::optional<std::pmr::string> title_;
  std::optional<std::pmr::string> artist_;
  std::pmr::vector<std::pmr::string> allArtists_;
  std::optional<std::pmr::string> album_;
  std::optional<std::pmr::string> album_artist_;
  std::optional<uint8_t> disc_;
  std::optional<uint16_t> track_;
  std::pmr::vector<std::pmr::string> genres_;
};

/*
 * Owning container for all of the metadata we store for a particular track.
 * This includes two main kinds of metadata:
 *  1. static(ish) attributes, such as the id, path on disk, hash of the tags
 *  2. dynamic attributes, such as the number of times this track has been
 *  played.
 *
 * Because a TrackData is immutable, it is thread safe but will not reflect any
 * changes to the dynamic attributes that may happen after it was obtained.
 *
 * Tracks may be 'tombstoned'; this indicates that the track is no longer
 * present at its previous location on disk, and we do not have any existing
 * files with a matching tags_hash. When this is the case, we ignore this
 * TrackData for most purposes. We keep the entry in our database so that we can
 * properly restore dynamic attributes (such as play count) if the track later
 * re-appears on disk.
 */
struct TrackData {
 public:
  TrackData()
      : id(0),
        filepath(),
        tags_hash(0),
        individual_tag_hashes(&memory::kSpiRamResource),
        is_tombstoned(false),
        modified_at(),
        last_position(0),
        play_count(0),
        type(MediaType::kUnknown) {}

  TrackId id;
  std::pmr::string filepath;
  uint64_t tags_hash;
  std::pmr::unordered_map<Tag, uint64_t> individual_tag_hashes;
  bool is_tombstoned;
  std::pair<uint16_t, uint16_t> modified_at;
  uint32_t last_position;
  uint32_t play_count;
  MediaType type;

  TrackData(const TrackData&& other) = delete;
  TrackData& operator=(TrackData& other) = delete;
  auto clone() const -> std::shared_ptr<TrackData>;

  bool operator==(const TrackData&) const = default;
};

/*
 * Immutable and owning combination of a track's tags and metadata.
 *
 * Note that instances of this class may have a fairly large memory impact, due
 * to the large number of strings they own. Prefer to query the database again
 * (which has its own caching layer), rather than retaining Track instances for
 * a long time.
 */
class Track {
 public:
  Track(std::shared_ptr<TrackData>& data, std::shared_ptr<TrackTags> tags)
      : data_(data), tags_(tags) {}

  Track(Track& other) = delete;
  Track& operator=(Track& other) = delete;

  bool operator==(const Track&) const = default;

  auto data() const -> const TrackData& { return *data_; }
  auto tags() const -> const TrackTags& { return *tags_; }

 private:
  std::shared_ptr<const TrackData> data_;
  std::shared_ptr<TrackTags> tags_;
};

}  // namespace database