summaryrefslogtreecommitdiff
path: root/src/database/include/track.hpp
blob: 620fc59ea1fae635bf1c558e1762ed8864771e37 (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
/*
 * Copyright 2023 jacqueline <me@jacqueline.id.au>
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */

#pragma once

#include <stdint.h>

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "leveldb/db.h"
#include "shared_string.h"
#include "span.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 Encoding {
  kUnsupported = 0,
  kMp3 = 1,
  kWav = 2,
  kOgg = 3,
  kFlac = 4,
};

enum class Tag {
  kTitle = 0,
  kArtist = 1,
  kAlbum = 2,
  kAlbumTrack = 3,
  kGenre = 4,
  kDuration = 5,
};

/*
 * Owning container for tag-related track metadata that was extracted from a
 * file.
 */
class TrackTags {
 public:
  auto encoding() const -> Encoding { return encoding_; };
  auto encoding(Encoding e) -> void { encoding_ = e; };

  TrackTags() : encoding_(Encoding::kUnsupported) {}

  std::optional<int> channels;
  std::optional<int> sample_rate;
  std::optional<int> bits_per_sample;

  std::optional<int> duration;

  auto set(const Tag& key, const std::string& val) -> void;
  auto at(const Tag& key) const -> std::optional<shared_string>;
  auto operator[](const Tag& key) const -> std::optional<shared_string>;

  /*
   * 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;

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

 private:
  Encoding encoding_;
  std::map<Tag, shared_string> tags_;
};

/*
 * Immutable 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.
 */
class TrackData {
 private:
  const TrackId id_;
  const std::string filepath_;
  const uint64_t tags_hash_;
  const uint32_t play_count_;
  const bool is_tombstoned_;

 public:
  /* Constructor used when adding new tracks to the database. */
  TrackData(TrackId id, const std::string& path, uint64_t hash)
      : id_(id),
        filepath_(path),
        tags_hash_(hash),
        play_count_(0),
        is_tombstoned_(false) {}

  TrackData(TrackId id,
            const std::string& path,
            uint64_t hash,
            uint32_t play_count,
            bool is_tombstoned)
      : id_(id),
        filepath_(path),
        tags_hash_(hash),
        play_count_(play_count),
        is_tombstoned_(is_tombstoned) {}

  auto id() const -> TrackId { return id_; }
  auto filepath() const -> std::string { return filepath_; }
  auto play_count() const -> uint32_t { return play_count_; }
  auto tags_hash() const -> uint64_t { return tags_hash_; }
  auto is_tombstoned() const -> bool { return is_tombstoned_; }

  auto UpdateHash(uint64_t new_hash) const -> TrackData;

  /*
   * Marks this track data as a 'tombstone'. Tombstoned tracks are not playable,
   * and should not generally be shown to users.
   */
  auto Entomb() const -> TrackData;

  /*
   * Clears the tombstone bit of this track, and updates the path to reflect its
   * new location.
   */
  auto Exhume(const std::string& new_path) const -> 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(const TrackData& data, const TrackTags& tags)
      : data_(data), tags_(tags) {}
  Track(const Track& other) = default;

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

  auto TitleOrFilename() const -> shared_string;

  bool operator==(const Track&) const = default;
  Track operator=(const Track& other) const { return Track(other); }

 private:
  const TrackData data_;
  const TrackTags tags_;
};

void swap(Track& first, Track& second);

}  // namespace database