2013-05-13 23:13:54 -04:00
|
|
|
===============================================================================
|
|
|
|
= =
|
|
|
|
= Ocarina 6.0 =
|
|
|
|
= =
|
|
|
|
===============================================================================
|
|
|
|
|
2013-06-19 14:07:36 -04:00
|
|
|
My main goal for Ocarina 6.x is to plan out all of my actions before writing
|
|
|
|
code. In the past I was adding features as I thought of them before thinking
|
|
|
|
out how everything works together, and this made Ocarina difficult to maintain
|
|
|
|
because I had no overall plan. This document aims to fix that.
|
2013-05-13 23:13:54 -04:00
|
|
|
|
2013-05-26 11:08:41 -04:00
|
|
|
I will also create unit tests as I add features so bugs can be found faster.
|
|
|
|
|
|
|
|
|
2013-05-13 23:13:54 -04:00
|
|
|
Files:
|
2013-05-26 11:08:41 -04:00
|
|
|
ocarina/
|
|
|
|
design.txt
|
|
|
|
ocarina/gui/
|
|
|
|
ocarina/include/
|
2013-06-27 16:20:38 -04:00
|
|
|
database.h
|
|
|
|
database.hpp
|
2013-06-27 23:48:09 -04:00
|
|
|
file.h
|
2013-06-21 18:18:53 -04:00
|
|
|
group.h
|
2013-06-27 23:03:14 -04:00
|
|
|
index.h
|
2013-05-26 11:08:41 -04:00
|
|
|
library.h
|
|
|
|
playlist.h
|
2013-06-28 00:12:56 -04:00
|
|
|
prefs.h
|
2013-05-26 11:08:41 -04:00
|
|
|
ocarina/lib/
|
2013-06-27 16:20:38 -04:00
|
|
|
database.cpp
|
2013-06-27 23:48:09 -04:00
|
|
|
file.cpp
|
2013-06-21 18:18:53 -04:00
|
|
|
group.cpp
|
2013-06-27 23:03:14 -04:00
|
|
|
index.cpp
|
2013-05-26 11:08:41 -04:00
|
|
|
library.cpp
|
|
|
|
playlist.cpp
|
2013-06-28 00:12:56 -04:00
|
|
|
prefs.cpp
|
2013-05-26 11:08:41 -04:00
|
|
|
ocarina/tests/
|
|
|
|
|
2013-06-19 14:07:36 -04:00
|
|
|
$HOME/.ocarina{-debug}/
|
2013-06-27 16:20:38 -04:00
|
|
|
album.db
|
|
|
|
artist.db
|
|
|
|
genre.db
|
2013-06-27 23:16:21 -04:00
|
|
|
groups.idx
|
2013-06-27 16:20:38 -04:00
|
|
|
library.db
|
|
|
|
track.db
|
|
|
|
|
|
|
|
|
|
|
|
|
2013-06-27 23:48:09 -04:00
|
|
|
On-disk files: (lib/file.cpp)
|
2013-06-27 16:20:38 -04:00
|
|
|
I use the disk to store data between sessions, this could include
|
|
|
|
library state and user preferences. In theory, file formats do not
|
|
|
|
change often so updating between file formats should be possible.
|
|
|
|
Supporting all previous file formats can create a lot of clutter in
|
|
|
|
the code, so I will ONLY support updating from the previous file-format
|
|
|
|
version. Ocarina 5.x used two different numbers to represent the
|
|
|
|
current file format (library = 2 and playlist = 3). I want to unify
|
|
|
|
this into a single number shared across all files for simplicity, and
|
|
|
|
then create a class to read and write data on disk.
|
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
Items should be written to a file with either a space or new line
|
|
|
|
separating multiple values.
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Notation:
|
2013-06-27 22:41:03 -04:00
|
|
|
File << aaaaa << bbbbb << endl is translated into "aaaaa bbbbb\n"
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- File version:
|
2013-06-27 23:48:09 -04:00
|
|
|
#define FILE_VERSION 4
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Open mode:
|
2013-06-27 23:48:09 -04:00
|
|
|
enum OpenMode {
|
|
|
|
OPEN_READ,
|
|
|
|
OPEN_WRITE,
|
|
|
|
}
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- File:
|
2013-06-27 23:48:09 -04:00
|
|
|
class File {
|
|
|
|
private:
|
|
|
|
union {
|
|
|
|
ifstream in;
|
|
|
|
ofstream out;
|
|
|
|
} f;
|
|
|
|
unsigned int version;
|
|
|
|
OpenMode mode;
|
|
|
|
string filepath;
|
|
|
|
public:
|
|
|
|
File(filepath);
|
|
|
|
open(OpenMode);
|
|
|
|
const File &operator<<(File &);
|
|
|
|
const File &operator>>(File &);
|
|
|
|
getline(string &);
|
|
|
|
}
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- File format:
|
2013-06-27 23:48:09 -04:00
|
|
|
File << FILE_VERSION << <OTHER_DATA>
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- API:
|
2013-06-27 23:48:09 -04:00
|
|
|
File : File(filepath)
|
|
|
|
Resolve filepath to ~/.ocarina{-debug}/filepath
|
|
|
|
File : open(mode)
|
|
|
|
Open a file for either reading or writing.
|
|
|
|
If reading: Read in version from start of file
|
|
|
|
Else write version to file
|
|
|
|
File : operator<<()
|
|
|
|
Write data to out
|
|
|
|
File : operator>>()
|
|
|
|
Read data from in;
|
|
|
|
File : getline(string);
|
|
|
|
getline(f.in, string) to read an entire line
|
|
|
|
|
2013-06-27 16:20:38 -04:00
|
|
|
|
|
|
|
|
|
|
|
Database: (lib/database.cpp)
|
|
|
|
Ocarina 5.x created a different save file format for each type of
|
|
|
|
data that needed to be stored (preferences, library paths, playlists).
|
|
|
|
I intend to unify everything into a generic file format that can be
|
|
|
|
accessed through a generic database interface. The database code will
|
|
|
|
be in charge of printing the "valid" bit for each DatabaseEntry so that
|
|
|
|
child classes do not need to call into the parent class. If valid ==
|
|
|
|
true, the DatabaseEntry will be streamed out followed by a newline. If
|
|
|
|
valid == false the database will print the next entry in the vector.
|
|
|
|
|
|
|
|
Modules should inherit from the DatabasEntry class and implement their
|
|
|
|
own read() and write() functions. The "valid" field will be stored
|
|
|
|
before these functions are called, and the entry will be skipped if
|
|
|
|
valid is set to false.
|
|
|
|
|
|
|
|
The Database class is a templated class, so code could potentially
|
|
|
|
get messy. Normal class declarations can still exist in the file
|
|
|
|
include/database.h and member functions can be written in the file
|
|
|
|
include/database.hpp, which will be included by database.h. Any
|
|
|
|
function not relying on a template can be written in lib/database.cpp.
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- DatabaseEntry:
|
2013-06-27 16:20:38 -04:00
|
|
|
class DatabaseEntry { /* let database modify valid flag */
|
|
|
|
private:
|
|
|
|
bool valid;
|
|
|
|
public:
|
2013-06-27 23:48:09 -04:00
|
|
|
virtual File &operator>>(File &) = 0;
|
|
|
|
virtual File &operator<<(File &) = 0;
|
2013-06-27 16:20:38 -04:00
|
|
|
friend class Database;
|
|
|
|
};
|
2013-05-26 11:08:41 -04:00
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
File << <CHILD_CLASS_DATA>
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Database:
|
2013-06-27 16:20:38 -04:00
|
|
|
template <class T>
|
|
|
|
class Database {
|
|
|
|
private:
|
|
|
|
unsigned int _size; /* Number of valid rows */
|
2013-06-27 23:48:09 -04:00
|
|
|
File filename;
|
2013-06-27 16:20:38 -04:00
|
|
|
vector<T> db;
|
|
|
|
public:
|
|
|
|
Database::Database(filename);
|
|
|
|
void load();
|
|
|
|
void save();
|
2013-06-27 23:16:21 -04:00
|
|
|
unsigned int insert(T);
|
2013-06-27 16:20:38 -04:00
|
|
|
void delete(unsigned int);
|
|
|
|
const unsigned int &size();
|
|
|
|
const T &operator[](unsigned int);
|
|
|
|
};
|
2013-05-26 11:08:41 -04:00
|
|
|
|
2013-06-27 23:48:09 -04:00
|
|
|
File << db.size() << endl
|
2013-06-27 22:41:03 -04:00
|
|
|
File << INDEX_0 << db[INDEX_0].valid << db[INDEX_0] << endl;
|
|
|
|
File << Index_1 << db[INDEX_1].valid << db[INDEX_1] << endl;
|
|
|
|
...
|
2013-06-27 16:20:38 -04:00
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- API:
|
2013-06-27 16:20:38 -04:00
|
|
|
Database.Database(filename);
|
|
|
|
Initializes database to use ~/.ocarina{-debug}/filename
|
|
|
|
Database.load();
|
|
|
|
Reads data from file. Call after static initialization of
|
|
|
|
Ocarina to ensure idle tasks are configured.
|
|
|
|
Database.save();
|
|
|
|
Saves data to file.
|
|
|
|
Database.insert(T &);
|
2013-06-27 23:16:21 -04:00
|
|
|
Adds a new item to the db, returns the id of the item
|
2013-06-27 16:20:38 -04:00
|
|
|
Database.delete(unsigned int index);
|
|
|
|
Mark db[index] as invalid (quick deletion)
|
|
|
|
Database.size();
|
|
|
|
Returns number of valid rows in the database
|
|
|
|
Database.operator[unsigned int index]
|
|
|
|
Return a reference to db[index]
|
2013-05-13 23:13:54 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
2013-06-27 23:03:14 -04:00
|
|
|
Index: (lib/index.cpp)
|
|
|
|
An inverted index allows me to map multiple values to a single key.
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Index:
|
2013-06-27 23:03:14 -04:00
|
|
|
class Index {
|
|
|
|
private:
|
|
|
|
map<string, set<int>>
|
2013-06-27 23:48:09 -04:00
|
|
|
File filename;
|
2013-06-27 23:03:14 -04:00
|
|
|
public:
|
|
|
|
Index::Index(filename);
|
|
|
|
void load();
|
|
|
|
void save();
|
|
|
|
void insert(key, int);
|
|
|
|
void delete(key, int);
|
2013-06-28 00:12:56 -04:00
|
|
|
void replace(key, int);
|
2013-06-27 23:16:21 -04:00
|
|
|
const set<string> &keys();
|
2013-06-27 23:03:14 -04:00
|
|
|
const set<int> &operator[](string);
|
|
|
|
};
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
File << key << endl;
|
|
|
|
File << map[key].size() << int_0 << int_1 << ... << int_n << endl;
|
2013-06-27 23:03:14 -04:00
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- API:
|
2013-06-27 23:03:14 -04:00
|
|
|
Index.Index(filename);
|
|
|
|
Initializes an index using ~/.ocarina{-debug}/filename
|
|
|
|
Index.load();
|
|
|
|
Reads data from a file. Call after static initialization of
|
|
|
|
Ocarina to ensure idle tasks are configured
|
|
|
|
Index.save();
|
|
|
|
Saves data to file
|
|
|
|
Index.insert(key, int);
|
|
|
|
1) If key does not exist, create it.
|
|
|
|
2) Add int to the list
|
|
|
|
3) Index.save()
|
|
|
|
Index.delete(key, int);
|
|
|
|
1) Remove int from the set of values associated with key
|
|
|
|
2) Do not delete key if set is empty
|
|
|
|
3) Index.save()
|
2013-06-28 00:12:56 -04:00
|
|
|
Index.replace(key, int)
|
|
|
|
1) map[key].clear()
|
|
|
|
2) insert(key, int)
|
2013-06-27 23:16:21 -04:00
|
|
|
Index.keys();
|
|
|
|
Return a set of index keys
|
2013-06-27 23:03:14 -04:00
|
|
|
Index.operator[](string key);
|
|
|
|
Return the set associated with key
|
|
|
|
|
|
|
|
|
|
|
|
|
2013-06-19 14:07:36 -04:00
|
|
|
Library: (lib/library.cpp)
|
2013-06-27 22:21:07 -04:00
|
|
|
The library manages databases containing track information added by the
|
|
|
|
user. Ocarina 6 splits the library into multiple database tables for
|
|
|
|
storing content. The library will exist in a library namespace to
|
|
|
|
to make functions and classes more unique.
|
2013-05-26 11:08:41 -04:00
|
|
|
|
2013-06-27 22:21:07 -04:00
|
|
|
When a library : Track is created, it should be added to the "Library"
|
|
|
|
group if it is NOT a member of the banned songs group.
|
2013-06-19 14:07:36 -04:00
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Album:
|
2013-06-27 22:21:07 -04:00
|
|
|
class library :: Album : public DatabaseEntry {
|
2013-06-19 14:07:36 -04:00
|
|
|
string name;
|
|
|
|
short year;
|
|
|
|
};
|
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
File << year << name
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Artist:
|
2013-06-27 22:21:07 -04:00
|
|
|
class library :: Artist : public DatabaseEntry {
|
2013-06-19 14:07:36 -04:00
|
|
|
string name;
|
|
|
|
};
|
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
File << name
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Genre:
|
2013-06-27 22:21:07 -04:00
|
|
|
class library :: Genre : public DatabaseEntry {
|
2013-06-21 18:18:53 -04:00
|
|
|
string name;
|
|
|
|
};
|
2013-05-26 11:08:41 -04:00
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
File << name
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Path:
|
2013-06-27 22:41:03 -04:00
|
|
|
class library :: Path : public DatabaseEntry {
|
2013-06-27 22:21:07 -04:00
|
|
|
string root_path;
|
2013-06-27 16:20:38 -04:00
|
|
|
bool enabled;
|
|
|
|
};
|
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
File << enabled << root_path
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Track:
|
2013-06-27 22:21:07 -04:00
|
|
|
class library :: Track : public DatabaseEntry {
|
2013-06-27 16:20:38 -04:00
|
|
|
unsigned int artist_id;
|
|
|
|
unsigned int album_id;
|
|
|
|
unsigned int genre_id;
|
|
|
|
unsigned int library_id;
|
|
|
|
|
|
|
|
short track;
|
|
|
|
short last_year;
|
|
|
|
short last_month;
|
|
|
|
short last_day;
|
|
|
|
unsigned int play_count;
|
|
|
|
unsigned int length;
|
|
|
|
string title;
|
|
|
|
string length_str;
|
|
|
|
string filepath;
|
|
|
|
};
|
|
|
|
|
2013-06-27 22:41:03 -04:00
|
|
|
File << artist_id << album_id << genre_id << library_id << track << last_year
|
|
|
|
File << last_year << last_month << last_day << play_count << length << endl
|
|
|
|
File << title << endl;
|
|
|
|
File << filepath << endl;
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Track: /* This struct lies outside the library namespace */
|
2013-06-27 22:41:03 -04:00
|
|
|
struct Track {
|
2013-06-27 22:21:07 -04:00
|
|
|
library :: Album *album;
|
|
|
|
library :: Artist *artist;
|
|
|
|
library :: Genre *genre;
|
|
|
|
library :: Library *library;
|
|
|
|
library :: Track *track;
|
|
|
|
};
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Databases:
|
2013-06-27 23:16:21 -04:00
|
|
|
Database<library :: Album> album_db(album.db);
|
|
|
|
Database<library :: Artist> artist_db(artist.db);
|
|
|
|
Database<library :: Album> genre_db(genre.db);
|
|
|
|
Database<library :: Library> library_db(library.db);
|
|
|
|
Database<library :: Track> track_db(track.db);
|
2013-06-27 16:20:38 -04:00
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Updating algorithm:
|
2013-06-27 22:21:07 -04:00
|
|
|
set<pair<lib_id, track_path>> known_tracks;
|
|
|
|
|
|
|
|
1) For each track currently in the library, check if the track exists
|
|
|
|
in the filesystem.
|
|
|
|
1a) If the track does exist, add to the known_tracks map.
|
|
|
|
1b) Else, mark track invalid.
|
|
|
|
2) For each file in the scan directory, check if (lib_id, track_path)
|
|
|
|
exists in the known_tracks map.
|
|
|
|
2a) If the file is in the map, do nothing.
|
2013-06-27 23:16:21 -04:00
|
|
|
2b) Else, add track to the library and to the "All Music" and
|
|
|
|
"Library" and "Unplayed Tracks" groups
|
2013-06-27 23:03:14 -04:00
|
|
|
3) Save all databases
|
2013-06-27 22:21:07 -04:00
|
|
|
|
|
|
|
The taglib library should be used for finding artist, album, etc. tags
|
|
|
|
for each track.
|
|
|
|
|
|
|
|
Use idle tasks for step 2 to break up tagging new files into chunks.
|
|
|
|
This way the user will still be able to use Ocarina and scanning can
|
|
|
|
happen while idle.
|
2013-05-26 11:08:41 -04:00
|
|
|
|
|
|
|
- API
|
2013-06-27 23:48:09 -04:00
|
|
|
library :: init();
|
|
|
|
Initialize all databases
|
2013-06-27 22:21:07 -04:00
|
|
|
library :: add_path(string dir);
|
2013-05-26 11:08:41 -04:00
|
|
|
Add new row to paths table, update
|
2013-06-27 22:21:07 -04:00
|
|
|
library :: del_path(unsigned int lib_id);
|
|
|
|
Invalidate a path row
|
|
|
|
library :: update_path(lib_id);
|
|
|
|
Update the given library path, if valid.
|
|
|
|
const Database<LibraryEntry> &library :: get_db();
|
|
|
|
Returns the database containing library information.
|
|
|
|
struct Track library :: resolve(track_id)
|
|
|
|
Fill out a Track structure for the provided track_id
|
2013-05-26 11:08:41 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
2013-06-27 23:16:21 -04:00
|
|
|
Groups: (lib/group.cpp)
|
|
|
|
Groups are going to be a new feature in Ocarina 6 and can compare
|
|
|
|
directly to Gmail-style labels. Ocarina 6 will create dynamic groups
|
|
|
|
that cannot be deleted by the user based on library status. Similar
|
|
|
|
to the library, groups should exist in their own namespace.
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Index:
|
2013-06-27 23:16:21 -04:00
|
|
|
Index group_idx(groups.idx)
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
- Default groups:
|
2013-06-27 23:16:21 -04:00
|
|
|
All music
|
|
|
|
All tracks are added to this group
|
|
|
|
Library
|
|
|
|
Banned Songs
|
|
|
|
These groups are mutually exclusive. A track is either
|
|
|
|
in the Library or the Banned Songs group
|
|
|
|
Unplayed tracks
|
|
|
|
Tracks with a play count of 0
|
|
|
|
|
|
|
|
- API
|
2013-06-27 23:48:09 -04:00
|
|
|
group :: init();
|
|
|
|
Initialize the index
|
2013-06-27 23:16:21 -04:00
|
|
|
group :: list();
|
|
|
|
return group_idx.keys();
|
|
|
|
group :: get_tracks(name):
|
|
|
|
return group_idx[name]
|
|
|
|
group :: add(name, track_id)
|
|
|
|
group_idx.insert(name, track_id);
|
|
|
|
group :: del(name, track_id)
|
|
|
|
grou_idx.delete(name, track_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
2013-06-28 00:12:56 -04:00
|
|
|
Preferences: (lib/prefs.cpp)
|
|
|
|
Preferences make use of a special index where the set<int> is always
|
|
|
|
size 1. Preferences will be in the prefs namespace.
|
|
|
|
|
|
|
|
- Index:
|
|
|
|
Index prefs(prefs.idx);
|
|
|
|
|
|
|
|
- API:
|
|
|
|
prefs :: set(string, val);
|
|
|
|
prefs.replace(string, val);
|
|
|
|
prefs :: get(string)
|
|
|
|
return prefs[string].begin()
|
|
|
|
|
|
|
|
|
|
|
|
|
2013-05-26 11:08:41 -04:00
|
|
|
Playlist: (lib/playlist.cpp)
|
|
|
|
A playlist is a simple list of songs that can be played either randomly
|
|
|
|
or in a user-defined order. It would probably be best to use a linked
|
|
|
|
list or vector to represent playlists, rather than creating a SQLite
|
|
|
|
table. I will be able to easily rearrange tracks in the playlist this
|
|
|
|
way. This will also make it easier to deal with playlist renames and
|
|
|
|
reordering by the user.
|
|
|
|
|
|
|
|
- API
|
|
|
|
/* Playlist management */
|
|
|
|
new_playlist();
|
|
|
|
del_playlist(playlist);
|
|
|
|
add_to_playlist(playlist, songid);
|
|
|
|
rm_from_playlist(playlist, songid);
|
|
|
|
playlist_size(playlist)
|
|
|
|
set_flag(playlist, flag)
|
|
|
|
|
|
|
|
- Flags
|
|
|
|
PL_ENABLED (1 << 0)
|
|
|
|
PL_RANDOM (1 << 1)
|
|
|
|
PL_DRAIN (1 << 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
2013-06-21 18:18:53 -04:00
|
|
|
Future work:
|
|
|
|
I want to set reasonable expectations for Ocarina 6 so that I don't
|
|
|
|
have to spend a large amount of time coding before releasing something
|
|
|
|
to the wild. This section will be a list of features that I want, but
|
|
|
|
should be deferred to a future release so basic support can be coded.
|
|
|
|
|
|
|
|
Hint: If feature B depends on A, implement A in 6.x and B in 6.x+1
|
|
|
|
|
|
|
|
- Categories: (6.1)
|
|
|
|
Use these to make "groups of groups" for better organization.
|
|
|
|
Different categories can include Album, Artist and Genere
|
|
|
|
dynamic groups in addition to user created groups (see below)
|
|
|
|
|
|
|
|
The Artist, Album and Genre "tables" can be used to populate
|
|
|
|
these categories.
|
|
|
|
|
|
|
|
- User created song groups: (6.2)
|
|
|
|
Basic add and remove features can be implemented using the
|
|
|
|
Library and Banned Songs groups. This will give me a chance
|
|
|
|
to test saving groups on a small scale before letting users
|
|
|
|
create anything they want.
|
|
|
|
|
|
|
|
- Save a user's playlist as a group: (6.2)
|
|
|
|
|
|
|
|
- Library defragment: (6.1)
|
|
|
|
Ocarina 6.0 will leave holes in the library when tracks are
|
|
|
|
deleted, potentially leading to fragmentation and larger-than-
|
|
|
|
needed file sizes. A "defragment" utility can be created to
|
|
|
|
clean up unused slots.
|
|
|
|
|
|
|
|
To help with fixing groups, a mapping of (old values) ->
|
|
|
|
(new values) should be kept.
|